公众号:程序员读书,欢迎关注
Web
开发是Go
语言使用的一个主要方向,使用Go
语言做Web
开发是一件很简单的事情,你甚至可以只用几行代码就可以启动一个Web Server
,这主要还是因为Go
标准库net/http
包对HTTP
协议的完善支持。
标准库net/http
包含Web
的Client
与Server
两个部分,在这里我们只讲与Server
相关的部分。
入门
在net/http
包里处理HTTP
请求函数方法都有一个统一的函数签名:
go
func (w http.ResponseWriter, r *http.Request)
可以看到,这个函数接收两个参数,分别对应HTTP
的请求(request
)与响应(response
):
http.ResponseWriter
:响应对象,实现了io.Writer
接口,用于将数据发送回客户端(比如浏览器)*http.Request
:请求对象,可以通过该对象获得Query
参数,表单数据,Cookies
,Headers
等HTTP
请求数据。
net/http
包的HandleFunc()
函数则可以将符合上面签名的函数添加到路由中,用于处理HTTP
请求:
go
func profile(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"this is profile page")
}
//将profile函数注册到路由
http.HandleFunc("/profile",profile)
//添加一个匿名函数用于处理请求
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, the url you are requesting is : %s\n", r.URL.Path)
})
路由注册后需要调用http.ListenAndServe()
函数启动Web
服务监听HTTP
请求:
go
http.ListenAndServe(":80", nil)
下面是一个启动简单Web
服务器的完整代码:
go
//main.go
package main
import (
"fmt"
"net/http"
)
func profile(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"this is profile page")
}
func main() {
http.HandleFunc("/profile",profile)
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, the url you are requesting is : %s\n", r.URL.Path)
})
http.ListenAndServe(":80", nil)
}
运行命令启动服务器:
shell
$ go run main.go
用curl
发送如下请求:
shell
$ curl http://localhost/hello
Hello, the url you are requesting is : /hello
$ curl http://localhost/profile
this is profile page
探究
Go
语言启动一个Web
服务器如此简单,原因在于net/http
包帮我们做了大量的封装。
下面我们来探究一下net/http
到底帮我们做了什么。
当我们把一个函数传给http.HandleFunc()
时,http.HandleFunc()
实际上是调用
DefaultServeMux
变量的HandleFunc()
方法:
go
//http包的HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
而DefaultServeMux
的HandleFunc()
函数会帮我们把传进去的函数转换为一个实现了http.HandlerFunc
函数:
go
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
http.HandlerFunc
函数实现了http.Handler
接口:
go
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
DefaultServeMux
的HandleFunc()
方法同时也会调用Handle()
方法将路由(pattern
)与转换后的函数做绑定后保存到一个map
当中:
go
func (mux *ServeMux) Handle(pattern string, handler Handler) {
//省略其他代码...
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
//省略其他代码...
}
我们调用http.HandleFunc()
函数的目的就是将路由与函数绑定后保存到DefaultServeMux
中。
DefaultServeMux
实际类型是ServeMux
,也叫多路复用器 ,DefaultServeMux
也实现了http.Handler
接口,ServeHTTP()
方法是最终处理HTTP
请求的地方。
接下来,当我们调用http.ListenAndServe()
函数时,在该函数中会创建一个http.Server
结构体类型,并调用该结构体的ListenAndServe()
方法监听请求:
go
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
http.Server
的ListenAndServe()
方法会一直监听,一旦有请求就会创建连接对象(Connection
),并且将http.Server
封装到http.serverHandler
中,在http.serverHandler
的ServeHTTP()
方法中调用http.Server
的Handler
处理请求:
go
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
http.ListenAndServe()
函数的第二个参数是http.Handler
,即我们可以自定义一个多路复用器并传递给ListenAndServe()
函数,这样就会调用用DefaultServeMux
了:
go
package main
import (
"fmt"
"net/http"
)
type MyMux struct {
}
func (m MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "this is my mux")
}
func main() {
http.ListenAndServe(":80", &MyMux{})
}
http.ListenAndServe()
函数实际是调用http.Server
的,所以我们也可以直接自定义创建一个http.Server
结构,这样可以自定义http.Server
结构体的字段(该结构体有挺多字段的)。
下面是一个自定义多路复用器以及自定义http.Server
来启动Web服务器的小案例:
go
package main
import (
"fmt"
"net/http"
)
type MyMux struct {
}
func (m MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "this is my mux")
}
func main() {
s := http.Server{
Addr: ":80",
Handler: MyMux{},
//默认为1M,这里改成2M
MaxHeaderBytes: 1 << 21,
}
s.ListenAndServe()
}
请求参数
在HTTP
请求中,要发送参数给服务器有两种方式,一种是将参数URL
的问号后面并用&
分隔的Query
,另一种是放在请求体(Body
)中的数据。
Query
Query
是指发送GET
请求,此时参数是跟在URL
问号后面的部分,各参数之间使用&
分隔:
shell
http://localhost?param1=value1¶m2=value2
要获得Query
参数,要先http.Request
的ParseForm()
方法解析URL
之后,解析 后的Query
参数被放在http.Request
的Form
字段中,这个字段的类型为url.Values
:
go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
id := r.Form["id"][0]
fmt.Println(id)
})
http.ListenAndServe(":80", nil)
}
另一种获取Query
参数的方法就是可以访问http.Request
的FormValue()
方法,这个方法会根据key
值自动调用ParseForm()
方法并从Form
字段中查找数据:
go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
id := r.FormValue("id")
fmt.Println(id)
})
http.ListenAndServe(":80", nil)
}
Body
当发送POST
、Put
或者PATCH
请求时,数据主要是通过HTTP
协议的Body
传递给服务器,而当请求头的Content-Type
的值不同时,对应的Body
数据格式也不同,这里我们讨论三种最常见的Content-Type
取值:
application/x-www-form-urlencoded
multipart/form-data
application/json
application/x-www-form-urlencoded
这种格式也就是一般我们所说的表单数据,有两种获取方式。
第一种方式与前面的获取Query
一样,通过http.Request
的Form
字段或者FormValue()
方法获取即可,因为http.Request
的ParseForm()
方法在解析参数时会把Body
和Query
的参数合在在Form
字段当中。
第二种方式是先调用http.Request
的ParseForm()
,然后从http.Request
的PostForm
字段取值,这个字段存储着表单数据。
go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
name := r.PostForm["name"][0]
fmt.Println(name)
})
http.ListenAndServe(":80", nil)
}
调用http.Request
的PostFormValue()
方法同样可以根据key
值自动调用ParseForm()
方法对应获取Body
中的数据:
go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
name := r.PostFormValue("name")
fmt.Println(name)
})
http.ListenAndServe(":80", nil)
}
multipart/form-data
Content-Type
为multipart/form-data
时,更多的时候是用于上传文件的,而关于文件上传的内容,我们在在这篇文章的后续小节会讲到。
当通过这种方式传递非文件数据时,其获取方式则与application/x-www-form-urlencoded
是一样的。
application/json
当Content-Type
为application/json
时,说明此时Body
里的数据格式为JSON
格式,这个时候我们就需要自己读取Body
的内容并反序列化到自己定义的数据结构中:
go
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
http.HandleFunc("/testJson", func(w http.ResponseWriter, r *http.Request) {
params, _ := ioutil.ReadAll(r.Body)
defer r.Body.Close()
var u User
json.Unmarshal(params, &u)
fmt.Println(u)
})
http.ListenAndServe(":80", nil)
}
模板处理
Go
的html/template
包提供了HTML
模板页面的动态解析,我们可以按照这个包的语法将HTML
动态渲染并返回给客户端:
go
package main
import (
"html/template"
"net/http"
)
func main() {
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
//模板
html := `{{.}},{{define "T"}}Hello, {{.}}!{{end}}`
//创建模板,解析模板
t, _ := template.New("foo").Parse()
//_ = t.ExecuteTemplate(w, "T", "ttt")
//模板输出
t.Execute(w, "This is a test page")
})
http.ListenAndServe(":80", nil)
}
下面是Go的html/template
包的一些常用模板语句:
模板语句 | 说明 |
---|---|
{{/* a comment */}} |
注释 |
{{.}} |
获取根变量 |
{{.Title}} |
访问名称为Title的变量 |
{{if .Done}} {{else}} {{end}} |
IF分支判断 |
{{range .Todos}} {{.}} {{end}} |
遍历Todos变量,使用{{.}} 访问Todos的每个子项 |
{{block "content" .}} {{end}} |
设置名称为"content"的模板 |
由于现在一般都是前后端分离,因此很少会使用这种模板来开发网页,因此在这里只要稍做了解即可。
JSON响应
如果我们开发的是API
接口,那么更常见的是返回JSON
数据,比如像下面这样的JSON
格式:
json
{
"code": 200,
"message": "success",
"data": [
{
"id": 1,
"name": "小明",
"class": "高中二年级二班"
},
{
"id": 2,
"name": "小花",
"class": "高中二年级三班"
},
{
"id": 3,
"name": "小海",
"class": "高中二年级二班"
}
]
}
Go
语言的encoding/json
包对JSON
做了非常好的支持,因为我们完全可以调用这个包来返回数据给客户端:
go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Student struct {
ID int `json:"id"`
Name string `json:"name"`
Class string `json:"class"`
}
type Result struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
func main() {
http.HandleFunc("/students", func(w http.ResponseWriter, r *http.Request) {
students := []Student{
{ID: 1, Name: "小明", Class: "高中二年级二班"},
{ID: 2, Name: "小花", Class: "高中二年级三班"},
{ID: 3, Name: "小海", Class: "高中二年级二班"},
}
rs := Result{Code: 200, Message: "success", Data: students}
rsJson, _ := json.MarshalIndent(rs, "", "\t")
fmt.Fprint(w, string(rsJson))
})
http.ListenAndServe(":80", nil)
}
静态资源
除了处理动态请求,Web服务器也需要处理静态资源请求,比如JS
,HTML
,CSS
等静态代码,或者是图片。
go
package main
import "net/http"
func main() {
fs := http.FileServer(http.Dir("static/"))
http.Handle("/assets/", http.StripPrefix("/assets/", fs))
http.ListenAndServe(":80", nil)
}
上面的示例中,我们将当前目录下的static
目录设置为静态资源目录,其访问路由前缀为assets
arduino
├── main.go
└── static
├── css
│ └── index.css
├── images
│ └── 1.png
└── js
└── index.js
文件上传
http.Request
的FormFile()
方法可以读取HTTP文件上传请求,该函数的签名如下:
go
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
函数的参数为HTTP
请求的文件字段名称,调用后返回multipart.File
和multipart.FileHeader
,下面是一个完整的文件上传小案例:
go
package main
import (
"io"
"net/http"
"os"
)
func main() {
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("无效请求"))
return
}
defer file.Close()
//最大为2M
max := 1 << 21
//大小限制
if header.Size > int64(max) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("文件大小不能超过2M"))
return
}
//读取ContentType
buffer := make([]byte, 512)
ff, _ := header.Open()
ff.Read(buffer)
contentType := http.DetectContentType(buffer)
//只允许上传png图片
if contentType != "image/png" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("文件格式错误"))
return
}
//把文件放在/upload目录下
f, err := os.Create("./upload/" + header.Filename)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("服务器内部错误"))
return
}
defer f.Close()
io.Copy(f, file)
w.Write([]byte("文件上传成功"))
})
http.ListenAndServe(":80", nil)
}
Cookie与Session
HTTP
协议是一种无状态的应用层协议,也就说HTTP
服务器无法判断两次HTTP
请求是否为同一个用户发送的,所以需要一种可以追踪用户的机制。
最常用的机制就是Cookie
和Session
。
Cookie
Cookie
是一种把追踪用户数据存放在客户端(一般是指浏览器)的机制,其原理是Web
服务器通过Set-Cookie
响应头 将数据发送给客户端,客户端保存起来,并且在之后的请求中通过Cookie
请求头 将该数据发送给Web
服务器。
go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/testCookie", func(w http.ResponseWriter, r *http.Request) {
cookieOne := http.Cookie{
Name: "c1",
Value: "this is c1",
}
cookieTwo := http.Cookie{
Name: "c2",
Value: "this is c2",
}
w.Header().Set("Set-Cookie", cookieOne.String())
w.Header().Add("Set-Cookie", cookieTwo.String())
fmt.Fprintln(w, "test")
})
http.ListenAndServe(":8080", nil)
}
Session
与Cookie
相比,Session
则是一种把用户数据存储在服务端的会话机制,当然使用Session
的前提是要能跟踪客户端,而Cookie
可以跟踪客户端,所以Session
一般跟Cookie
配合使用。
Go
标准库并没有提供操作Session
的实现,而Session
也仅仅是把数据存储在服务端而已,所以自己实现一个就可以了。
不过为了避免重复造轮子,还是推荐使用第三方库,比如github.com/gorilla/sessions
:
go
package main
import (
"net/http"
"os"
"github.com/gorilla/sessions"
)
//SESSION_KEY不要直接保存在源码里
//可以放在命令行或者环境变量中
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
func main() {
http.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
session.Values["foo"] = "bar"
session.Values[42] = 43
err := session.Save(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
http.ListenAndServe(":80", nil)
}
小结
使用Go语言进行Web开发是一种非常简单的事情,使用标准库net/http
包就可以开发一个完整的Web
应用程序,再引出其他第三方库,比如路由,session
等,则使我们的应用程序更加完善。