go-Web学习笔记
准备
go安装
vscode
go插件
REST Client
vs code自带的http请求插件:REST Client,REST Client 是一个 VS Code 扩展插件,它允许你发送 HTTP 请求并直接在 VS Code 上查看响应结果,类似postman。
简单demo
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world"))
})
http.ListenAndServe("localhost:8080", nil) // default serve mux
}
处理(Handle)请求
创建web server底层用的是server
结构体的函数:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
Handler
是一个接口:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
创建web server时如果handler为nil, 就会使用defaultservemux,一个默认的分发器:
if handler == nil {
handler = DefaultServeMux
}
ServeMux维护了handler的映射,实现了ServerHTTP方法,也就是实现了Handler接口:
作用:

自定义一个Handler
type myHandler struct{}
func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hi you"))
}
func main() {
mh := &myHandler{}
server := http.Server{
Addr: "localhost:8080",
Handler: mh,
}
server.ListenAndServe()
}
简单的demo,作为一个分发器,至少要存储映射关系。
映射
type helloHandler struct{}
func (m *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hi you"))
}
type aboutHandler struct{}
func (m *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("about"))
}
func main() {
server := http.Server{
Addr: "localhost:8080",
Handler: nil,
}
http.Handle("/hello", &helloHandler{})
http.Handle("/about", &aboutHandler{})
server.ListenAndServe()
}
HandleFunc
可以实现Handle同样的功能;可以将具有合适参数的函数f,适配成一个Handler,而这个Handler具有方法f。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
// type Handler interface {
// ServeHTTP(ResponseWriter, *Request)
// }
// type HandlerFunc func(ResponseWriter, *Request) 函数类型
// func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
// f(w, r)
// }
mux.Handle(pattern, HandlerFunc(handler))
}
函数类型和接口函数
最后还是调用的handle来实现,这里面牵扯到一个知识点:接口型函数
一个接口,只有一个方法,紧接着定义了同参数的函数类型,这个函数类型还定义了接口里的方法,并且调用了自己,就是一个实现了接口的函数类型,简称接口型函数。
换句话说:以上面的例子为例,
HandlerFunc(handler)
把自定义函数转化为函数类型(参数一样,转没毛病),而这个函数类型实现了Handler
接口。这样,就能使用结构体作为参数,也可以使用普通的函数作为参数,使用更加灵活,可读性也更好,这就是接口函数的价值。
也就是可以这样使用:
http.HandleFunc("/home", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home"))
})
Handler
内置handler
http.NotFoundHandler()
http.RedirectHandler
跳转。
http.StripPrefix
从请求url去掉指定前缀,再调用handler,如果请求的url和提供的前缀不符,404。
http.TimeoutHandler
在指定时间内运行传入的handler。
http.FileServer
使用基于root的文件系统来响应请求。
Request
type Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header // type Header map[string][]string
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{}
Response *Response
ctx context.Context
}
HTTP Request 和 HTTP Response
如果是浏览器发出的请求,会把Fragment部分(也就是#后面的部分)去掉。
header
server := http.Server{
Addr: "localhost:8080",
Handler: nil,
}
http.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.Header)
fmt.Fprintln(w, r.Header["Accept-Encoding"])
fmt.Fprintln(w, r.Header.Get("Accept-Encoding"))
})
server.ListenAndServe()
REST Client直接在vscode里面跑下试试:
body
http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
length := r.ContentLength
body := make([]byte, length)
r.Body.Read(body)
fmt.Fprintln(w, string(body))
})
查询参数

http.HandleFunc("/param", func(w http.ResponseWriter, r *http.Request) {
s := r.URL.RawQuery
fmt.Printf("rawquery: %v\n", s)
query := r.URL.Query() //map[string][]string
id := query["id"]
threadID := query.Get("thread_id")
fmt.Printf("id: %v\n", id)
fmt.Printf("threadID: %v\n", threadID)
})
// GET http://localhost:8080/param?id=hqinglau&thread_id=123 HTTP/1.1
// rawquery: id=hqinglau&thread_id=123
// id: [hqinglau]
// threadID: 123
Form
HTML表单里面的数据会以键值对的形式,通过POST请求发送出去。数据内容放在POST请求的BODY里面。
<form action="127.0.0.1:8080/process" method="post" enctype="multipart/form-data">
<input type="text" name="name1"/>
<input type="text" name="name2"/>
<input type="submit"/>
</form>
表单的enctype属性
默认是
application/x-www-form-urlencoded
,浏览器会将表单数据编码到查询字符串里面:例如param?id=hqinglau&thread_id=123
如果是
multipart/form-data
,每个键值对都会被转换成一个MIME消息部分,每一部分有自己的Content Type和Content Disposition
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm() //先解析表单application/x-www-form-urlencoded
r.ParseMultipartForm(1000) // multipart/form-data
fmt.Fprintln(w, r.Form)
fmt.Fprintln(w, r.MultipartForm)
// &{map[name1:[hqinglau] name2:[orzlinux.cn]] map[]}
// 第二个map是文件,为空
})
如果只要表单里的键值对,不要url的,可以使用PostForm()
(只支持application/x-www-form-urlencoded
)
fmt.Fprintln(w, r.FormValue("name1")) // hqinglau
上传文件
<form action="http://127.0.0.1:8080/process?hello=world&thread=123" method="post" enctype="multipart/form-data">
<input type="text" name="hello" value="hqing lau"/>
<input type="text" name="post" value="456"/>
<input type="file" name="uploaded"/>
<input type="submit"/>
</form>
go
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fh := r.MultipartForm.File["uploaded"][0]
f, err := fh.Open()
if err == nil {
data, _ := ioutil.ReadAll(f)
fmt.Fprintln(w, string(data))
}
})
http.ResponseWriter
一个问题:
func(w http.ResponseWriter, r *http.Request)
为何前面不是指针,因为它是个接口,接口是引用类型。
值类型分别有:int系列、float系列、bool、string、数组和结构体
引用类型有:指针、slice切片、管道channel、接口interface、map、函数等
值类型的特点是:变量直接存储值,内存通常在栈中分配
引用类型的特点是:变量存储的是一个地址,这个地址对应的空间里才是真正存储的值,内存通常在堆中分配。
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error) // 接收一个字节切片作为参数,然后写入到HTTP响应的body里面
WriteHeader(statusCode int)
}
如果write之前,header里面没有设定content type
,那么数据的前512字节就会用来检测content type。
例如:
func writeExample(w http.ResponseWriter, r *http.Request) {
str := `
<!DOCTYPE html>
<body>
<form action="http://127.0.0.1:8080/process?hello=world&thread=123" method="post" enctype="multipart/form-data">
<input type="text" name="hello" value="hqing lau"/>
<input type="text" name="post" value="456"/>
<input type="file" name="uploaded"/>
<input type="submit"/>
</form>
</body>
`
w.Write([]byte(str))
}
func main() {
server := http.Server{
Addr: "localhost:8080",
Handler: nil,
}
http.HandleFunc("/write", writeExample)
server.ListenAndServe()
}
Header可以进行添加:
http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "http://google.com")
w.WriteHeader(302) //之后header就不可更改了
})
json:
type Post struct {
User string
Threads []string
}
func main() {
server := http.Server{
Addr: "localhost:8080",
Handler: nil,
}
http.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
post := &Post{
User: "hqinglau",
Threads: []string{"first", "second", "third"},
}
b, _ := json.Marshal(post)
w.Write(b)
})
server.ListenAndServe()
}
模板
web模板就是预先设计好的HTML页面,可以被模板引擎反复使用,来产生HTML页面。
用到再说。
路由
静态路由
一个路径对应一个页面。
之前的handle都是放在main函数里面:
main()
: 设置类工作
controller
: 静态资源,把不同的请求送到不同的controller进行处理。
例子:
带参数的路由
根据路由参数,创建出一族不同的页面
/companies/123
/companies/Micro
demo:
func registerCompanyRoutes() {
http.HandleFunc("/companies", handleCompanies)
// 如果来了一个/companies/123,是走下面的,因为更具体了
http.HandleFunc("/companies/", handleCompany)
}
func handleCompanies(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "manyCompany")
}
func handleCompany(w http.ResponseWriter, r *http.Request) {
pattern, _ := regexp.Compile(`/companies/(\d+)`)
s := pattern.FindStringSubmatch(r.URL.Path)
if len(s) > 0 {
i, _ := strconv.Atoi(s[1])
fmt.Fprintln(w, i)
} else {
fmt.Fprintln(w, "404")
}
}
还有很多第三方路由器,如httprouter
、gorilla/mux
等。
JSON
go结构体如果要导出,字段名要大写,而json一般小写,要二者映射起来的话,需要:
// 属性名映射
type Company struct {
ID int `json:"id`
Name string `json:"name`
Country string `json:"country`
}
类型映射:
go bool: json boolean
go float64: json 数值
go string: json strings
go nil: json null
对于未知结构的 json:
map[string]interface{}
可以存储任意JSON对象
[]interface{}
可以存储任意的 JSON 数组
读写JSON
编解码器,适合流,如web应用:
type Company struct {
ID int `json:"id"`
Name string `json:"name"`
Country string `json:"country"`
}
func main() {
http.HandleFunc("/tt", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
dec := json.NewDecoder(r.Body)
company := Company{}
err := dec.Decode(&company)
if err != nil {
fmt.Printf("err.Error(): %v\n", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
enc := json.NewEncoder(w)
err = enc.Encode(company)
if err != nil {
fmt.Printf("err.Error(): %v\n", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
})
http.ListenAndServe("localhost:8080", nil)
}
结果:
marshal
和 unmarshal
编解码。
中间件
放在handler之前。
用途:
- 日志
- 安全,如身份认证
- 请求超时
- 响应压缩
demo:
type AuthMiddleWare struct {
Next http.Handler
}
func (m AuthMiddleWare) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if m.Next == nil {
m.Next = http.DefaultServeMux
}
auth := r.Header.Get("Authorization")
if auth != "" {
m.Next.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
type Company struct {
ID int `json:"id"`
Name string `json:"name"`
Country string `json:"country"`
}
func main() {
http.HandleFunc("/tt", func(w http.ResponseWriter, r *http.Request) {
company := Company{
Name: "Google",
Country: "USA",
ID: 23,
}
enc := json.NewEncoder(w)
enc.Encode(company)
})
http.ListenAndServe("localhost:8080", new(AuthMiddleWare))
}
结果:
请求上下文
// 只读
type Context interface {
Deadline() (deadline time.Time, ok bool) // 什么时候变成失效的
Done() <-chan struct{} // 一旦被取消了,就会接收到一个信号
Err() error
Value(key any) any // 从context获取信息
}
修改的话,有一些方法,返回一个新的context:

例如设置超时判断:
type TimeoutMiddleware struct {
Next http.Handler
}
func (m TimeoutMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if m.Next == nil {
m.Next = http.DefaultServeMux
}
// 获取request上下文
ctx := r.Context()
// 修改ctx超时时间
ctx, _ = context.WithTimeout(ctx, time.Second*3)
// 使用新ctx代替原来的
r.WithContext(ctx)
ch := make(chan struct{})
go func() {
m.Next.ServeHTTP(w, r)
// 如果三秒内完成,就会给通道一个信号
ch <- struct{}{}
}()
select {
case <-ch:
return
case <-ctx.Done():
w.WriteHeader(http.StatusRequestTimeout)
}
ctx.Done()
}
type Company struct {
ID int `json:"id"`
Name string `json:"name"`
Country string `json:"country"`
}
func main() {
http.HandleFunc("/tt", func(w http.ResponseWriter, r *http.Request) {
company := Company{
Name: "Google",
Country: "USA",
ID: 23,
}
// 设置延迟几秒
time.Sleep(time.Second * 4)
enc := json.NewEncoder(w)
enc.Encode(company)
})
http.ListenAndServe("localhost:8080", new(TimeoutMiddleware))
}
测试GO WEB
user**_test**.go
- 测试代码文件以
_test
结尾 - 对于生产编译,不会包含以
_test
结尾的文件 - 对于测试编译,会包含以
_test
结尾的文件
func TestUpdatesModifiedTime(t *testing.T) {...}
- 测试函数名以
Test
开头,需要导出 - 函数名需要表达出被验证的特性
- 参数类型问题,提供了测试相关的一些工具
简单例子:
测试Model层
company.go
package model
import "strings"
type Company struct {
ID int `json:"id"`
Name string `json:"name"`
Country string `json:"country"`
}
func (c *Company) GetCompanyType() (result string) {
if strings.HasSuffix(c.Name, ".LTD") {
result = "Limited Liability Company"
} else {
result = "others"
}
return
}
company_test.go
如何测试:
go test -timeout 30s -run ^TestCompanyTypeCorrect$ go_pro/model
测试Controller层
- 为了保证单元测试的隔离性,测试不要使用数据库,外部API、文件系统等外部资源。
- 模拟请求和响应,需要使用
net/http/httptest
提供的功能
company.go
func registerCompanyRoutes() {
http.HandleFunc("/companies", handleCompanies)
}
func handleCompanies(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "manyCompany")
}
company_test.go
性能分析
import _ "net/http/pprof"
// 设置一些监听的URL,会提供各类诊断信息
命令行:

网页:
单独跑一个8000端口,然后在网址打开。