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
}

image-20220420160042811

处理(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接口:

image-20220420161931776

作用:

image-20220420162010986

自定义一个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()
}

image-20220420162607774

简单的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()
}

image-20220420163228741

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部分(也就是#后面的部分)去掉。

image-20220420190251630

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里面跑下试试:

image-20220420191232119

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))
})

image-20220420191740673

查询参数

image-20220420192619806
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

    image-20220420194053633

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()
}

image-20220420203221225

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()
}

image-20220420204711451

模板

web模板就是预先设计好的HTML页面,可以被模板引擎反复使用,来产生HTML页面。

用到再说。

路由

静态路由

一个路径对应一个页面。

之前的handle都是放在main函数里面:

image-20220420214517043

main(): 设置类工作

controller: 静态资源,把不同的请求送到不同的controller进行处理。

image-20220420214727919

例子:

image-20220420220021780

带参数的路由

根据路由参数,创建出一族不同的页面

/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")
    }
}

还有很多第三方路由器,如httproutergorilla/mux等。

JSON

image-20220420233035516

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)
}

结果:

image-20220420234441065

marshalunmarshal编解码。

中间件

放在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))
}

结果:

image-20220421091409254

请求上下文

// 只读
type Context interface {
    Deadline() (deadline time.Time, ok bool)  // 什么时候变成失效的
    Done() <-chan struct{} // 一旦被取消了,就会接收到一个信号
    Err() error
    Value(key any) any // 从context获取信息
}

修改的话,有一些方法,返回一个新的context:

image-20220421092213494

例如设置超时判断:

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))
}

image-20220421093145363

测试GO WEB

user**_test**.go

  • 测试代码文件以_test结尾
  • 对于生产编译,不会包含以_test结尾的文件
  • 对于测试编译,会包含以_test结尾的文件
func TestUpdatesModifiedTime(t *testing.T) {...}
  • 测试函数名以Test开头,需要导出
  • 函数名需要表达出被验证的特性
  • 参数类型问题,提供了测试相关的一些工具

简单例子:

image-20220421095351863

测试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

image-20220421095442368

如何测试:

go test -timeout 30s -run ^TestCompanyTypeCorrect$ go_pro/model

测试Controller层

  • 为了保证单元测试的隔离性,测试不要使用数据库,外部API、文件系统等外部资源。
  • 模拟请求和响应,需要使用net/http/httptest提供的功能

image-20220421100626612

company.go

func registerCompanyRoutes() {
    http.HandleFunc("/companies", handleCompanies)
}

func handleCompanies(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "manyCompany")
}

company_test.go

image-20220421100713612

性能分析

import _ "net/http/pprof"
// 设置一些监听的URL,会提供各类诊断信息

命令行:

image-20220421101214638

网页:

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

参考

软件工艺师