build-web-application-with-golang学习笔记

智慧并不产生于学历,而是来自对于知识的终生不懈的追求。这篇文章主要讲述build-web-application-with-golang学习笔记相关的知识,希望能为你提供帮助。
build-web-application-with-golang 学习教程
这几周学习以上教程,仅记录一些重点难点部分。
Go语言 Go语言基础Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个:

breakdefaultfuncinterfaceselect casedefergomapstruct chanelsegotopackageswitch constfallthroughifrangetype continue forimportreturnvar

  • var和const参考2.2Go语言基础里面的变量和常量申明
  • package和import已经有过短暂的接触
  • func 用于定义函数和方法
  • return 用于从函数返回
  • defer 用于类似析构函数
  • go 用于并发
  • select 用于选择不同类型的通讯
  • interface 用于定义接口,参考2.6小节
  • struct 用于定义抽象数据类型,参考2.5小节
  • break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
  • chan用于channel通讯
  • type用于声明自定义类型
  • map用于声明map类型数据
  • range用于读取slice、map、channel数据
Go程序是通过package来组织的,package < pkgName> 这一行告诉我们当前文件属于哪个包,而包名main则告诉我们它是一个可独立运行的包,它在编译后会产生可执行文件。除了main包之外,其它的包最后都会生成*.a文件(也就是包文件)并放置在$GOPATH/pkg/$GOOS_$GOARCH中(以Mac为例就是$GOPATH/pkg/darwin_amd64)。
每一个可独立运行的Go程序,必定包含一个package main,在这个main包中必定包含一个入口函数main,而这个函数既没有参数
,也没有返回值。
包的概念和python中的package类似,它们都有一些特别的好处:模块化(能够把你的程序分成多个模块)和可重用性(每个模块都能被其它应用程序反复使用)。
Go语言重点难点 interface
简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。
package mainimport "fmt"type Human struct { name string age int phone string }type Student struct { Human //匿名字段 school string loan float32 }type Employee struct { Human //匿名字段 company string money float32 }//Human实现SayHi方法 func (h Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) }//Human实现Sing方法 func (h Human) Sing(lyrics string) { fmt.Println("La la la la...", lyrics) }//Employee重载Human的SayHi方法 func (e Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) }// Interface Men被Human,Student和Employee实现 // 因为这三个类型都实现了这两个方法 type Men interface { SayHi() Sing(lyrics string) }func main() { mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}//定义Men类型的变量i var i Men//i能存储Student i = mike fmt.Println("This is Mike, a Student:") i.SayHi() i.Sing("November rain")//i也能存储Employee i = tom fmt.Println("This is tom, an Employee:") i.SayHi() i.Sing("Born to be wild")//定义了slice Men fmt.Println("Let‘s use a slice of Men and see what happens") x := make([]Men, 3) //这三个都是不同类型的元素,但是他们实现了interface同一个接口 x[0], x[1], x[2] = paul, sam, mikefor _, value := range x{ value.SayHi() } }

interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现。
空interface
空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。
// 定义a为空接口 var a interface{} var i int = 5 s := "Hello world" // a可以存储任意类型的数值 a = i a = s

goroutine
goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数。
package mainimport ( "fmt" "runtime" )func say(s string) { for i := 0; i < 5; i++ { runtime.Gosched() fmt.Println(s) } }func main() { go say("world") //开一个新的Goroutines执行 say("hello") //当前Goroutines执行 }// 以上程序执行后将输出: // hello // world // hello // world // hello // world // hello // world // hello

channels
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel:
package mainimport "fmt"func sum(a []int, c chan int) { total := 0 for _, v := range a { total += v } c < - total// send total to c }func main() { a := []int{7, 2, 8, -9, 4, 0}c := make(chan int) go sum(a[:len(a)/2], c) go sum(a[len(a)/2:], c) x, y := < -c, < -c// receive from cfmt.Println(x, y, x + y) }

Web Service With Golang Go搭建一个Web服务器Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的就搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。
package mainimport ( "fmt" "net/http" "strings" "log" )func sayhelloName(w http.ResponseWriter, r *http.Request) { r.ParseForm()//解析参数,默认是不会解析的 fmt.Println(r.Form)//这些信息是输出到服务器端的打印信息 fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) for k, v := range r.Form { fmt.Println("key:", k) fmt.Println("val:", strings.Join(v, "")) } fmt.Fprintf(w, "Hello astaxie!") //这个写入到w的是输出到客户端的 }func main() { http.HandleFunc("/", sayhelloName) //设置访问的路由 err := http.ListenAndServe(":9090", nil) //设置监听的端口 if err != nil { log.Fatal("ListenAndServe: ", err) } }

看到上面这个代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了(类似于Python的tornado)。我们build之后,然后执行web.exe,这个时候其实已经在9090端口监听http链接请求了。
在浏览器输入http://localhost:9090
可以看到浏览器页面输出了Hello astaxie!
可以换一个地址试试:http://localhost:9090/?url_long=111& url_long=222
看看浏览器输出的是什么,服务器输出的是什么?
build-web-application-with-golang学习笔记

文章图片

Go的Web服务底层
build-web-application-with-golang学习笔记

文章图片

http包执行流程
  1. 创建Listen Socket, 监听指定的端口, 等待客户端请求到来。
  2. Listen Socket接受客户端的请求, 得到Client Socket, 接下来通过Client Socket与客户端通信。
  3. 处理客户端的请求, 首先从Client Socket读取HTTP请求的协议头, 如果是POST方法, 还可能要读取客户端提交的数据, 然后交给相应的handler处理请求, handler处理完毕准备好客户端需要的数据, 通过Client Socket写给客户端。
这整个的过程里面我们只要了解清楚下面三个问题,也就知道Go是如何让Web运行起来了
  • 如何监听端口?
  • 如何接收客户端请求?
  • 如何分配handler?
前面小节的代码里面我们可以看到,Go是通过一个函数ListenAndServe来处理这些事情的,这个底层其实这样处理的:初始化一个server对象,然后调用了net.Listen("tcp", addr),也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口。
下面代码来自Go的http包的源码,通过下面的代码我们可以看到整个的http处理过程:
func (srv *Server) Serve(l net.Listener) error { defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { rw, e := l.Accept() if e != nil { if ne, ok := e.(net.Error); ok & & ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c, err := srv.newConn(rw) if err != nil { continue } go c.serve() } }

监控之后如何接收客户端的请求呢?上面代码执行监控端口之后,调用了srv.Serve(net.Listener)函数,这个函数就是处理接收客户端的请求信息。这个函数里面起了一个for{},首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个goroutine,把这个请求的数据当做参数扔给这个conn去服务:go c.serve()。这个就是高并发体现了,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。
那么如何具体分配到相应的函数来处理请求呢?conn首先会解析request:c.readRequest(),然后获取相应的handler:handler := c.server.Handler,也就是我们刚才在调用函数ListenAndServe时候的第二个参数,我们前面例子传递的是nil,也就是为空,那么默认获取handler = DefaultServeMux,那么这个变量用来做什么的呢?对,这个变量就是一个路由器,它用来匹配url跳转到其相应的handle函数,那么这个我们有设置过吗?有,我们调用的代码里面第一句不是调用了http.HandleFunc("/", sayhelloName)嘛。这个作用就是注册了请求/的路由规则,当请求uri为"/",路由就会转到函数sayhelloName,DefaultServeMux会调用ServeHTTP方法,这个方法内部其实就是调用sayhelloName本身,最后通过写入response的信息反馈到客户端。
详细的整个流程如下图所示:
build-web-application-with-golang学习笔记

文章图片

Go代码的执行流程通过对http包的分析之后,现在让我们来梳理一下整个的代码执行过程。
  • 首先调用Http.HandleFunc
    按顺序做了几件事:
    1 调用了DefaultServeMux的HandleFunc
    2 调用了DefaultServeMux的Handle
    3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则
  • 其次调用http.ListenAndServe(":9090", nil)
    按顺序做了几件事情:
    1 实例化Server
    2 调用Server的ListenAndServe()
    3 调用net.Listen("tcp", addr)监听端口
    4 启动一个for循环,在循环体中Accept请求
    5 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve()
    6 读取每个请求的内容w, err := c.readRequest()
    7 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux
    8 调用handler的ServeHttp
    9 在这个例子中,下面就进入到DefaultServeMux.ServeHttp
    10 根据request选择handler,并且进入到这个handler的ServeHTTP
    mux.handler(r).ServeHTTP(w, r)

    11 选择handler:
    A 判断是否有路由能满足这个request(循环遍历ServeMux的muxEntry)
    B 如果有路由满足,调用这个路由handler的ServeHTTP
    C 如果没有路由满足,调用NotFoundHandler的ServeHTTP
表单表单是我们平常编写Web应用常用的工具,通过表单我们可以方便的让客户端和服务器进行数据的交互。
表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(
)定义。
处理表单输入
默认情况下,Handler里面是不会自动解析form的,必须显式的调用r.ParseForm()后,你才能对这个表单数据进行操作。
r.Form里面包含了所有请求的参数,比如URL中query-string、POST的数据、PUT的数据,所以当你在URL中的query-string字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。
request.Form是一个url.Values类型,里面存储的是对应的类似key=value的信息,下面展示了可以对form数据进行的一些操作:
v := url.Values{} v.Set("name", "Ava") v.Add("friend", "Jess") v.Add("friend", "Sarah") v.Add("friend", "Zoe") // v.Encode() == "name=Ava& friend=Jess& friend=Sarah& friend=Zoe" fmt.Println(v.Get("name")) fmt.Println(v.Get("friend")) fmt.Println(v["friend"])

验证表单输入
我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,这里关注如何在服务器端验证。
处理文件上传
【build-web-application-with-golang学习笔记】在服务器端,我们增加一个handlerFunc:
http.HandleFunc("/upload", upload)// 处理/upload 逻辑 func upload(w http.ResponseWriter, r *http.Request) { fmt.Println("method:", r.Method) //获取请求的方法 if r.Method == "GET" { crutime := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) token := fmt.Sprintf("%x", h.Sum(nil))t, _ := template.ParseFiles("upload.gtpl") t.Execute(w, token) } else { r.ParseMultipartForm(32 < < 20) file, handler, err := r.FormFile("uploadfile") if err != nil { fmt.Println(err) return } defer file.Close() fmt.Fprintf(w, "%v", handler.Header) f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)// 此处假设当前目录下已存在test目录 if err != nil { fmt.Println(err) return } defer f.Close() io.Copy(f, file) } }

通过上面的代码可以看到,处理文件上传我们需要调用r.ParseMultipartForm,里面的参数表示maxMemory,调用ParseMultipartForm之后,上传的文件存储在maxMemory大小的内存里面,如果文件大小超过了maxMemory,那么剩下的部分将存储在系统的临时文件中。我们可以通过r.FormFile获取上面的文件句柄,然后实例中使用了io.Copy来存储文件。
获取其他非文件字段信息的时候就不需要调用r.ParseForm,因为在需要的时候Go自动会去调用。而且ParseMultipartForm调用一次之后,后面再次调用不会再有效果。
通过上面的实例我们可以看到我们上传文件主要三步处理:
  1. 表单中增加enctype="multipart/form-data"
  2. 服务端调用r.ParseMultipartForm,把上传的文件存储在内存和临时文件中
  3. 使用r.FormFile获取文件句柄,然后对文件进行存储等处理。
访问数据库 使用mysql数据库
Go中支持MySQL的驱动目前比较多,有如下几种,有些是支持database/sql标准,而有些是采用了自己的实现接口,常用的有如下几种:
  • https://github.com/go-sql-driver/mysql 支持database/sql,全部采用go写。
  • https://github.com/ziutek/mymysql 支持database/sql,也支持自定义的接口,全部采用go写。
  • https://github.com/Philio/GoMySQL 不支持database/sql,自定义接口,全部采用go写。
接下来的例子我主要以第一个驱动为例(我目前项目中也是采用它来驱动),也推荐大家采用它,主要理由:
  • 这个驱动比较新,维护的比较好
  • 完全支持database/sql接口
  • 支持keepalive,保持长连接,虽然星星fork的mymysql也支持keepalive,但不是线程安全的,这个从底层就支持了keepalive。
示例代码接下来的几个小节里面我们都将采用同一个数据库表结构:数据库test,用户表userinfo,关联用户信息表userdetail。
CREATE TABLE `userinfo` ( `uid` INT(10) NOT NULL AUTO_INCREMENT, `username` VARCHAR(64) NULL DEFAULT NULL, `departname` VARCHAR(64) NULL DEFAULT NULL, `created` DATE NULL DEFAULT NULL, PRIMARY KEY (`uid`) ); CREATE TABLE `userdetail` ( `uid` INT(10) NOT NULL DEFAULT ‘0‘, `intro` TEXT NULL, `profile` TEXT NULL, PRIMARY KEY (`uid`) )

如下示例将示范如何使用database/sql接口对数据库表进行增删改查操作
package mainimport ( "database/sql" "fmt" //"time"_ "github.com/go-sql-driver/mysql" )func main() { db, err := sql.Open("mysql", "astaxie:[email  protected]/test?charset=utf8") checkErr(err)//插入数据 stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") checkErr(err)res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") checkErr(err)id, err := res.LastInsertId() checkErr(err)fmt.Println(id) //更新数据 stmt, err = db.Prepare("update userinfo set username=? where uid=?") checkErr(err)res, err = stmt.Exec("astaxieupdate", id) checkErr(err)affect, err := res.RowsAffected() checkErr(err)fmt.Println(affect)//查询数据 rows, err := db.Query("SELECT * FROM userinfo") checkErr(err)for rows.Next() { var uid int var username string var department string var created string err = rows.Scan(& uid, & username, & department, & created) checkErr(err) fmt.Println(uid) fmt.Println(username) fmt.Println(department) fmt.Println(created) }//删除数据 stmt, err = db.Prepare("delete from userinfo where uid=?") checkErr(err)res, err = stmt.Exec(id) checkErr(err)affect, err = res.RowsAffected() checkErr(err)fmt.Println(affect)db.Close()}func checkErr(err error) { if err != nil { panic(err) } }

通过上面的代码我们可以看出,Go操作Mysql数据库是很方便的。
关键的几个函数我解释一下:
sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式:
[email  protected](/path/to/socket)/dbname?charset=utf8 user:[email  protected](localhost:5555)/dbname?charset=utf8 user:[email  protected]/dbname user:[email  protected]([de:ad:be:ef::ca:fe]:80)/dbname

db.Prepare()函数用来返回准备要执行的sql操作,然后返回准备完毕的执行状态。
db.Query()函数用来直接执行Sql返回Rows结果。
stmt.Exec()函数用来执行stmt准备好的SQL语句
我们可以看到我们传入的参数都是=?对应的数据,这样做的方式可以一定程度上防止SQL注入。
使用Beego orm库进行ORM开发
beego orm是我开发的一个Go进行ORM操作的库,它采用了Go style方式对数据库进行操作,实现了struct到数据表记录的映射。beego orm是一个十分轻量级的Go ORM框架,开发这个库的本意降低复杂的ORM学习曲线,尽可能在ORM的运行效率和功能之间寻求一个平衡,beego orm是目前开源的Go ORM框架中实现比较完整的一个库,而且运行效率相当不错,功能也基本能满足需求。
beego orm是支持database/sql标准接口的ORM库,所以理论上来说,只要数据库驱动支持database/sql接口就可以无缝的接入beego orm。目前我测试过的驱动包括下面几个:
Mysql: github/go-mysql-driver/mysql
PostgreSQL: github.com/lib/pq
SQLite: github.com/mattn/go-sqlite3
Mysql: github.com/ziutek/mymysql/godrv
首先你需要import相应的数据库驱动包、database/sql标准接口包以及beego orm包,如下所示:
import ( "database/sql" "github.com/astaxie/beego/orm" _ "github.com/go-sql-driver/mysql" )func init() { //注册驱动 orm.RegisterDriver("mysql", orm.DR_MySQL) //设置默认数据库 orm.RegisterDataBase("default", "mysql", "root:[email  protected]/my_db?charset=utf8", 30) //注册定义的model orm.RegisterModel(new(User))// 创建table orm.RunSyncdb("default", false, true) }

导入必须的package之后,我们需要打开到数据库的链接,然后创建一个beego orm对象(以MySQL为例),如下所示
beego orm:
func main() { o := orm.NewOrm() }

简单示例:
package mainimport ( "fmt" "github.com/astaxie/beego/orm" _ "github.com/go-sql-driver/mysql" // 导入数据库驱动 )// Model Struct type User struct { Idint Name string `orm:"size(100)"` }func init() { // 设置默认数据库 orm.RegisterDataBase("default", "mysql", "root:[email  protected]/my_db?charset=utf8", 30)// 注册定义的 model orm.RegisterModel(new(User)) //RegisterModel 也可以同时注册多个 model //orm.RegisterModel(new(User), new(Profile), new(Post))// 创建 table orm.RunSyncdb("default", false, true) }func main() { o := orm.NewOrm()user := User{Name: "slene"}// 插入表 id, err := o.Insert(& user) fmt.Printf("ID: %d, ERR: %v\n", id, err)// 更新表 user.Name = "astaxie" num, err := o.Update(& user) fmt.Printf("NUM: %d, ERR: %v\n", num, err)// 读取 one u := User{Id: user.Id} err = o.Read(& u) fmt.Printf("ERR: %v\n", err)// 删除表 num, err = o.Delete(& u) fmt.Printf("NUM: %d, ERR: %v\n", num, err) }

session和数据存储 文本文件处理 Web服务 安全与加密 国际化和本地化 错误处理,调试和测试 部署与维护 如何设计一个Web框架 扩展Web框架





    推荐阅读