Go 是一门新语言。不少人都是用 Go 来开发 Web 服务。Web 开发不少同窗急于求成,直接使用 beego, echo 或 iris 等知名框架。对标准库 net/http
的了解甚少。这里我就主要聊一下标准库 net/http
开发 Web 服务时的使用细节。html
在 Go 中,建立 HTTP 服务很简单:git
package main // in main.go import ( "fmt" "net/http" ) func main(){ if err := http.ListenAndServe(":12345",nil); err != nil{ fmt.Println("start http server fail:",err) } }
这样就会启动一个 HTTP 服务在端口 12345。浏览器输入 http://localhost:12345/
就能够访问。固然从代码看出,没有给这个 HTTP 服务添加实际的处理逻辑,全部的访问都是默认的 404 Not Found
。github
<!--more-->golang
添加 HTTP 的处理逻辑的方法简单直接:web
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, Go HTTP Server")) }) http.HandleFunc("/abc", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, Go HTTP abc")) }) if err := http.ListenAndServe(":123456", nil); err != nil { fmt.Println("start http server fail:", err) } }
访问 http://localhost:12345/
就能够看到页面输出 Hello, Go HTTP Server
的内容。访问 http://localhost:12345/abc
就能够看到页面输出 Hello, Go HTTP abc
的内容。可是 Go 默认的路由匹配机制很弱。上面的代码除了 /abc
,其余的请求都匹配到 /
,不足以使用,确定须要本身写路由的过程。一个简单的方式就是写一个本身的 http.Handler
。算法
type MyHandler struct{} // 实现 http.Handler 接口的 ServeHTTP 方法 func (mh MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/abc" { w.Write([]byte("abc")) return } if r.URL.Path == "/xyz" { w.Write([]byte("xyz")) return } w.Write([]byte("index")) // 这里你能够写本身的路由匹配规则 } func main() { http.Handle("/", MyHandler{}) if err := http.ListenAndServe(":12345", nil); err != nil { fmt.Println("start http server fail:", err) } }
这样能够在本身的 MyHandler
写复杂的路由规则和处理逻辑。http.ListenAndServe
的第二个参数写的会更优雅:json
func main() { if err := http.ListenAndServe(":12345", MyHandler{}); err != nil { fmt.Println("start http server fail:", err) } }
net/http
提供了一个很是简单的路由结构 http.ServeMux
。方法 http.HandleFunc()
和 http.Handler()
就是把路由规则和对应函数注册到默认的一个 http.ServeMux
上。固然,你能够本身建立 http.ServeMux
来使用:数组
func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, HTTP Server") } func main() { mux := http.NewServeMux() mux.HandleFunc("/", handler) http.ListenAndServe(":12345", mux) }
可是由于 http.ServeMux
路由规则简单,功能有限,实践都不会用的,如同鸡肋。更推荐使用 httprouter。浏览器
import ( "fmt" "net/http" "github.com/julienschmidt/httprouter" ) // httprouter.Params 是匹配到的路由参数,好比规则 /user/:id 中 的 :id 的对应值 func handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "hello, httprouter") } func main() { router := httprouter.New() router.GET("/", handle) if err := http.ListenAndServe(":12345", router); err != nil { fmt.Println("start http server fail:", err) } }
Go 的 HTTP 处理过程能够不只是一个 http.HandlerFunc
,并且是一组 http.HandlerFunc
,好比:安全
func handle1(w http.ResponseWriter, r *http.Request) { w.Write([]byte("handle1")) } func handle2(w http.ResponseWriter, r *http.Request) { w.Write([]byte("handle2")) } // 把几个函数组合起来 func makeHandlers(handlers ...http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { for _, handler := range handlers { handler(w, r) } } } func main() { http.HandleFunc("/", makeHandlers(handle1, handle2)) if err := http.ListenAndServe(":12345", nil); err != nil { fmt.Println("start http server fail:", err) } }
这种模式开发的框架能够参考 negroni。它的中间件都是以实现 http.Handler
的结构体来组合的。
HTTP 过程的操做主要是针对客户端发来的请求数据在 *http.Request
,和返回给客户端的 http.ResponseWriter
两部分。
请求数据 *http.Request
有两个部分:基本数据和传递参数。基本数据好比请求的方法、协议、URL、头信息等能够直接简单获取:
func HttpHandle(w http.ResponseWriter, r *http.Request) { fmt.Println("Method:", r.Method) fmt.Println("URL:", r.URL, "URL.Path", r.URL.Path) // 这里是 *net/url.URL 结构,对应的内容能够查API fmt.Println("RemoteAddress", r.RemoteAddr) fmt.Println("UserAgent", r.UserAgent()) fmt.Println("Header.Accept", r.Header.Get("Accept")) fmt.Println("Cookies",r.Cookies()) // 还有不少确定会有的基本数据,能够查阅 API 找寻一下 } http.HandleFunc("/", HttpHandle) if err := http.ListenAndServe(":12345", nil); err != nil { fmt.Println("start http server fail:", err) }
请求传递的参数,也就是 表单数据,保存在 *http.Request.Form
和 *http.Request.PostForm
(POST 或 PUT 的数据),相似 PHP 的 $_REQUEST 和 $_POST/$_PUT 两个部分。
例如 GET /?abc=xyz
,获取这个数据并打印到返回内容:
func HttpHandle(w http.ResponseWriter, r *http.Request) { value := r.FormValue("abc") w.Write([]byte("GET: abc=" + value)) }
访问 http://localhost:12345/?abc=123
就能够看到页面内容 GET: abc=123
。POST 的表单数据也是相似:
func HttpHandle(w http.ResponseWriter, r *http.Request) { v1 := r.FormValue("abc") v2 := r.PostFormValue("abc") v3 := r.Form.Get("abc") v4 := r.PostForm.Get("abc") fmt.Println(v1 == v2, v1 == v3, v1 == v4) w.Write([]byte("POST: abc=" + v1)) }
注意,这四个值 v1,v2,v3,v4
是相同的值。
若是同一个表单域传递了多个值,须要直接操做 r.Form
或 r.PostForm
,好比 GET /?abc=123&abc=abc&abc=xyz
:
func HttpHandle(w http.ResponseWriter, r *http.Request) { // 这里必定要记得 ParseForm,不然 r.Form 是空的 // 调用 r.FormValue() 的时候会自动执行 r.ParseForm() r.ParseForm() values := r.Form["abc"] w.Write([]byte("GET abc=" + strings.Join(values, ","))) // 这里记得 import "strings" }
访问 http://localhost:12345/?abc=123&abc=abc&abc=xyz
能够看到内容 GET abc=123,abc,xyz
。
表单数据存储在 r.Form
,是 map[string][]string
类型,即支持一个表单域多个值的状况。r.FormValue() 只获取第一个值。
表单数据是简单的 kv 对应,很容易实现 kv 到 结构体的一一对应,例如使用库 https://github.com/mholt/binding:
type User struct { Id int Name string } func (u *User) FieldMap(req *http.Request) binding.FieldMap { return binding.FieldMap{ &u.Id: "user_id", &u.Name: binding.Field{ Form: "name", Required: true, }, } } func handle(w http.ResponseWriter, r *http.Request) { user := new(User) errs := binding.Bind(r, user) if errs.Handle(w) { return } }
不管表单数据,仍是上传的二进制数据,都是保存在 HTTP 的 Body 中的。操做 *http.Request.Body
能够获取到内容。可是注意 *http.Request.Body
是 io.ReadCloser
类型,只能一次性读取完整,第二次就是空的。
func HttpHandle(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Println("read body fail:", err) w.WriteHeader(500) return } DoSomething(body) // 尽可能使用已经读出的 body 内容,不要再去读取 r.Body w.Write([]byte("body:")) w.Write(body) }
使用 curl
命令行发送 POST 数据到服务器,curl -X POST --data "abcdefg" http://localhost:12345/
,能够看到返回内容 body:abcdefg
。
根据 HTTP 协议,若是请求的 Content-Type: application/x-www-form-urlencoded
,Body 中的数据就是相似 abc=123&abc=abc&abc=xyz
格式的数据,也就是常规的 表单数据。这些使用 r.ParseForm()
而后操做 r.Form
处理数据。若是是纯数据,好比文本abcdefg
、 JSON 数据等,你才须要直接操做 Body 的。好比接收 JSON 数据:
type User struct { Id int `json:"id"` Name string `json:"name"` } func HttpHandle(w http.ResponseWriter, r *http.Request) { // Body 里的内容是 JSON 数据: // {"id":123,"name":"xyz"} body, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Println("read body fail:", err) w.WriteHeader(500) return } var u User if err = json.Unmarshal(body, &u); err != nil { fmt.Println("json Unmarshal fail:", err) w.WriteHeader(500) return } w.Write([]byte("user.id:" + strconv.Itoa(u.Id) + " ")) w.Write([]byte("user.name:" + u.Name)) // 返回内容是: user.id:123 user.name:xyz }
若是须要对 Body 的数据作直接处理,JSON 数据例子是通用的模式。
上传的文件通过 Go 的解析保存在 *http.Request.MultipartForm
中,经过 r.FormFile()
去获取收到的文件信息和数据流,并处理:
func HttpHandle(w http.ResponseWriter, r *http.Request) { // 这里必定要记得 r.ParseMultipartForm(), 不然 r.MultipartForm 是空的 // 调用 r.FormFile() 的时候会自动执行 r.ParseMultipartForm() r.ParseMultipartForm(32 << 20) // 写明缓冲的大小。若是超过缓冲,文件内容会被放在临时目录中,而不是内存。过大可能较多占用内存,太小可能增长硬盘 I/O // FormFile() 时调用 ParseMultipartForm() 使用的大小是 32 << 20,32MB file, fileHeader, err := r.FormFile("file") // file 是上传表单域的名字 if err != nil { fmt.Println("get upload file fail:", err) w.WriteHeader(500) return } defer file.Close() // 此时上传内容的 IO 已经打开,须要手动关闭!! // fileHeader 有一些文件的基本信息 fmt.Println(fileHeader.Header.Get("Content-Type")) // 打开目标地址,把上传的内容存进去 f, err := os.OpenFile("saveto.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { fmt.Println("save upload file fail:", err) w.WriteHeader(500) return } defer f.Close() if _, err = io.Copy(f, file); err != nil { fmt.Println("save upload file fail:", err) w.WriteHeader(500) return } w.Write([]byte("upload file:" + fileHeader.Filename + " - saveto : saveto.txt")) }
上传文件信息中,文件大小 信息是没有的。而文件大小是上传限制中必要的条件,因此须要一些方法来获取文件大小:
// 使用接口检查是否有 Size() 方法 type fileSizer interface { Size() int64 } // 从 multipart.File 获取文件大小 func getUploadFileSize(f multipart.File) (int64, error) { // 从内存读取出来 // if return *http.sectionReader, it is alias to *io.SectionReader if s, ok := f.(fileSizer); ok { return s.Size(), nil } // 从临时文件读取出来 // or *os.File if fp, ok := f.(*os.File); ok { fi, err := fp.Stat() if err != nil { return 0, err } return fi.Size(), nil } return 0, nil }
r.FormFile()
只返回第一个上传的文件,若是同一个表单域上传多个文件,只能直接操做 r.MultipartForm
:
func HttpHandle(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(32 << 20) var ( file multipart.File err error ) for _, fileHeader := range r.MultipartForm.File["file"] { if file, err = fileHeader.Open(); err != nil { fmt.Println("open upload file fail:", fileHeader.Filename, err) continue } SaveFile(file) // 仿照上面单个文件的操做,处理 file file.Close() // 操做结束必定要 Close,for 循环里不要用 defer file.Close() file = nil w.Write([]byte("save:" + fileHeader.Filename + " ")) } }
http.ResponseWriter
是一个接口,你能够根据接口,添加一些本身须要的行为:
type ResponseWriter interface { Header() Header // 添加返回头信息 Write([]byte) (int, error) // 添加返回的内容 WriteHeader(int) // 设置返回的状态码 }
w.WriteHeader()
是一次性的,不能重复设置状态码,不然会有提示信息:
func HttpHandle(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) // 设置成功 w.WriteHeader(404) // 提示:http: multiple response.WriteHeader calls w.WriteHeader(503) // 提示:http: multiple response.WriteHeader calls }
并且须要在 w.Write()
以前设置 w.WriteHeader()
,不然是 200。(要先发送状态码,再发送内容)
func HttpHandle(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World")) w.WriteHeader(404) // 提示:http: multiple response.WriteHeader calls,由于 w.Write() 已发布 HTTP 200 }
http.ResponseWriter
接口过于简单,实际使用会本身实现 ResponseWriter
来使用,好比获取返回的内容:
type MyResponseWriter struct { http.ResponseWriter bodyBytes *bytes.Buffer } // 覆写 http.ResponseWriter 的方法 func (mrw MyResponseWriter) Write(body []byte) (int, error) { mrw.bodyBytes.Write(body) // 记录下返回的内容 return mrw.ResponseWriter.Write(body) } // Body 获取返回的内容,这个是本身添加的方法 func (mrw MyResponseWriter) Body() []byte { return mrw.bodyBytes.Bytes() } func HttpHandle(w http.ResponseWriter, r *http.Request) { m := MyResponseWriter{ ResponseWriter: w, bodyBytes: bytes.NewBuffer(nil), } m.Header().Add("Content-Type", "text/html") // 要输出HTML记得加头信息 m.Write([]byte("<h1>Hello World</h1>")) m.Write([]byte("abcxyz")) fmt.Println("body:", string(m.Body())) }
net/http
提供一些便利的方法能够输出其余的内容,好比 cookie:
func HttpHandle(w http.ResponseWriter, r *http.Request) { c := &http.Cookie{ Name: "abc", Value: "xyz", Expires: time.Now().Add(1000 * time.Second), MaxAge: 1000, HttpOnly: true, } http.SetCookie(w, c) }
好比服务端返回下载文件:
func HttpHandle(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "download.txt") }
或者是生成的数据流,好比验证码,看成文件返回:
func HttpHandle(w http.ResponseWriter, r *http.Request) { captchaImageBytes := createCaptcha() // 假设生成验证码的函数,返回 []byte buf := bytes.NewReader(captchaImageBytes) http.ServeContent(w, r, "captcha.png", time.Now(), buf) }
还有一些状态码的直接操做:
func HttpHandle(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/abc", 302) } func HttpHandle2(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) }
返回 JSON, XML 和 渲染模板的内容等的代码例子,能够参考 HTTP Response Snippets for Go。
Go 1.7 添加了 context
包,用于传递数据和作超时、取消等处理。*http.Request
添加了 r.Context()
和 r.WithContext()
来操做请求过程须要的 context.Context
对象。
context
能够在 http.HandleFunc
之间传递数据:
func handle1(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), "abc", "xyz123") // 写入 string 到 context handle2(w, r.WithContext(ctx)) // 传递给下一个 handleFunc } func handle2(w http.ResponseWriter, r *http.Request) { str, ok := r.Context().Value("abc").(string) // 取出的 interface 须要推断到 string if !ok { str = "not string" } w.Write([]byte("context.abc = " + str)) } func main() { http.HandleFunc("/", handle1) if err := http.ListenAndServe(":12345", nil); err != nil { fmt.Println("start http server fail:", err) } }
利用 context.WithTimeout
能够建立会超时结束的 context,用来处理业务超时的状况:
func HttpHandle(w http.ResponseWriter, r *http.Request) { ctx, cancelFn := context.WithTimeout(r.Context(), 1*time.Second) // cancelFn 关掉 WithTimeout 里的计时器 // 若是 ctx 超时,计时器会自动关闭,可是若是没有超时就执行到 <-resCh,就须要手动关掉 defer cancelFn() // 把业务放到 goroutine 执行, resCh 获取结果 resCh := make(chan string, 1) go func() { // 故意写业务超时 time.Sleep(5 * time.Second) resCh <- r.FormValue("abc") }() // 看 ctx 超时仍是 resCh 的结果先到达 select { case <-ctx.Done(): w.WriteHeader(http.StatusGatewayTimeout) w.Write([]byte("http handle is timeout:" + ctx.Err().Error())) case r := <-resCh: w.Write([]byte("get: abc = " + r)) } }
Go 的不少 HTTP 框架使用 context
或者本身定义的 Context
结果做为 http.Handler
中间件之间数据传递的媒介,好比 xhandler:
import( "context" "github.com/rs/xhandler" ) type myMiddleware struct { next xhandler.HandlerC } func (h myMiddleware) ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request) { ctx = context.WithValue(ctx, "test", "World") h.next.ServeHTTPC(ctx, w, r) } func main() { c := xhandler.Chain{} c.UseC(func(next xhandler.HandlerC) xhandler.HandlerC { return myMiddleware{next: next} }) xh := xhandler.HandlerFuncC(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { value := ctx.Value("test").(string) // 使用 context 传递的数据 w.Write([]byte("Hello " + value)) }) http.Handle("/", c.Handler(xh)) // 将 xhandler.Handler 转化为 http.Handler if err := http.ListenAndServe(":12345", nil); err != nil { fmt.Println("start http server fail:", err) } }
xhandler
封装 ServeHTTPC(ctx context.Context, w http.ResponseWriter, r *http.Request)
用于相似 http.Handler
的 ServeHTTP(w http.ResponseWriter, r *http.Request)
的行为,处理 HTTP 的过程。
一些时候须要直接操做 Go 的 HTTP 链接时,使用 Hijack()
将 HTTP 对应的 TCP 取出。链接在 Hijack()
以后,HTTP 的相关操做会受到影响,链接的管理须要用户本身操做,并且例如 w.Write([]byte)
不会返回内容,须要操做 Hijack()
后的 *bufio.ReadWriter
。
func HttpHandle(w http.ResponseWriter, r *http.Request) { hj, ok := w.(http.Hijacker) if !ok { return } conn, buf, err := hj.Hijack() if err != nil { w.WriteHeader(500) return } defer conn.Close() // 须要手动关闭链接 w.Write([]byte("hello")) // 会提示 http: response.Write on hijacked connection // 返回内容须要 buf.WriteString("hello") buf.Flush() }
Hijack
主要看到的用法是对 HTTP 的 Upgrade 时在用,好比从 HTTP 到 Websocket 时,golang.org/x/net/websocket:
func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { rwc, buf, err := w.(http.Hijacker).Hijack() if err != nil { panic("Hijack failed: " + err.Error()) } // The server should abort the WebSocket connection if it finds // the client did not send a handshake that matches with protocol // specification. defer rwc.Close() conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) if err != nil { return } if conn == nil { panic("unexpected nil conn") } s.Handler(conn) }
上面全部的代码我都是用的 http.ListenAndServe
来启动 HTTP 服务。实际上执行这个过程的 *http.Server
这个结构。有些时候咱们不是使用默认的行为,会给 *http.Server
定义更多的内容。
http.ListenAndServe
默认的 *http.Server
是没有超时设置的。一些场景下你必须设置超时,不然会遇到太多链接句柄的问题:
func main() { server := &http.Server{ Handler: MyHandler{}, // 使用实现 http.Handler 的结构处理 HTTP 数据 ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } // 监听 TCP 端口,把监听器交给 *http.Server 使用 ln, err := net.Listen("tcp", ":12345") if err != nil { panic("listen :12345 fail:" + err.Error()) } if err = server.Serve(ln); err != nil { fmt.Println("start http server fail:", err) } }
有朋友用 Beego 的时候但愿同时监听两个端口提供同样数据操做的 HTTP 服务。这个需求就能够利用 *http.Server
来实现:
import ( "fmt" "net/http" "github.com/astaxie/beego" "github.com/astaxie/beego/context" ) func main() { beego.Get("/", func(ctx *context.Context) { ctx.WriteString("abc") }) go func() { // server 的 ListenAndServe 是阻塞的,应该在另外一个 goroutine 开启另外一个server server2 := &http.Server{ Handler: beego.BeeApp.Handlers, // 使用实现 http.Handler 的结构处理 HTTP 数据 Addr: ":54321", } if err := server2.ListenAndServe(); err != nil { fmt.Println("start http server2 fail:", err) } }() server1 := &http.Server{ Handler: beego.BeeApp.Handlers, // 使用实现 http.Handler 的结构处理 HTTP 数据 Addr: ":12345", } if err := server1.ListenAndServe(); err != nil { fmt.Println("start http server1 fail:", err) } }
这样访问 http://localhost:12345
和 http://localhost:54321
均可以看到返回 abc
的内容。
随着互联网安全的问题日益严重,许多的网站开始使用 HTTPS 提供服务。Go 建立一个 HTTPS 服务是很简便的:
import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, HTTPS Server") } func main() { http.HandleFunc("/", handler) http.ListenAndServeTLS(":12345", "server.crt", "server.key", nil) }
ListenAndServeTLS
新增了两个参数 certFile
和 keyFile
。HTTPS的数据传输是加密的。实际使用中,HTTPS利用的是对称与非对称加密算法结合的方式,须要加密用的公私密钥对进行加密,也就是 server.crt
和 server.key
文件。具体的生成能够阅读 openssl
的文档。
关于 Go 和 HTTPS 的内容,能够阅读 Tony Bai 的 Go 和 HTTPS。
Go 的 net/http
包为开发者提供不少便利的方法的,能够直接开发不复杂的 Web 应用。若是须要复杂的路由功能,及更加集成和简便的 HTTP 操做,推荐使用一些 Web 框架。
各类 Web 框架 : awesome-go#web-frameworks
我的小站:fuxiaohei.me