美文网首页程序员
《Go语言入门经典》16~18章读书笔记

《Go语言入门经典》16~18章读书笔记

作者: 跑马溜溜的球 | 来源:发表于2020-07-02 16:03 被阅读0次

    第16章调试

    16.1 日志

    日志并非为报告Bug而提供的,而是可供在Bug发生时使用的基础设施。

    Go语言提供了log包,让应用程序能够将日志写入终端或文件。下面是一个简单的程序,它向终端输出一条日志消息。

    package main
    
    import (
        "log"
    )
    
    func main() {
        log.Printf("This is a log message");
    } 
    

    运行结果

    2020/06/30 19:26:59 This is a log message
    

    要将日志写入文件,可使用Go语言本身提供的功能,也可使用操作系统提供的功能。将日志写入文件的示例如下。

    package main
    
    import (
        "log"
        "os"
    )
    
    func main() {
        f, err := os.OpenFile("mylog", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
        if err != nil {
            log.Fatal(err)
        }
    
        defer f.Close()
    
        log.SetOutput(f)
    
        for i:=1; i<=5; i++{
            log.Printf("Log %d", i);
        }
    }
    

    16.3 使用fmt包

    fmt包可用来设置格式,因此必要时可使用它来输出数据,以方便调试。通过使用函数Printf,可创建要打印的字符串,并使用百分符号在其中引用变量。fmt包将对变量进行分析,并输出字符串。

    package main
    
    import (
        "fmt"
    )
    
    type Movie struct {
        Name string
        Rating float32
    }
    
    func main() {
        var m Movie
        m.Name = "Lunar"
        m.Rating = 9.2
    
        fmt.Printf("%+v\n", m);
    }
    

    %v表示是类型的默认格式,+表示打印结构体中字段的名称。

    16.4 使用Delve

    Go语言没有官方调试器,但很多社区项目都提供了Go语言调试器。Delve就是一个这样的项目,它为Go项目提供了丰富的调试环境。

    安装方式

    go get github.com/go-delve/delve/cmd/dlv
    

    或者

    cd $GOPATH/src/
    git clone https://github.com/derekparker/delve.git
    cd delve/cmd/dlv/
    go build
    go install
    

    假设有文件func.go

    package main 
    
    import (
        "fmt"
    )
    
    func IsEven(i int) bool {
        return i%2 == 0
    }
    
    func getPrize() (int, string) {
        i := 2
        s := "goldfish"
    
        return i, s
    }
    
    func sayHi() (x string, y string) {
        x = "hello"
        y = "world"
        return
    }
    
    func main() {
        str1, str2 := sayHi()
        fmt.Println(str1, str2)
        fmt.Println(IsEven(2))
    }
    

    执行如下命令进入调试

    dlv debug func.go
    

    常用命令:

    • b + 函数名/ b + 行号: 设置断点
    • bp:列出所有断点
    • c: 运行到下一个断点
    • clearall:清除所有断点
    • funcs:函数列表
    • p:打印
    • s: 单步执行
    • clear:清除单个断点

    例:

    (dlv) funcs main
    main.IsEven
    main.main
    main.sayHi
    runtime.main
    runtime.main.func1
    runtime.main.func2
    

    funcs main列出函数,注意只有调用的函数列会被列出,也只有被调用的函数才能设断点。

    (dlv) b main.IsEven
    Breakpoint 1 set at 0x4adad0 for main.IsEven() ./func.go:7
    (dlv) b func.go:20
    Breakpoint 2 set at 0x4adb15 for main.sayHi() ./func.go:20
    

    在 main.IsEven和文件的第20行上设置断点

    (dlv) c
    > main.sayHi() ./func.go:20 (hits goroutine(1):1 total:1) (PC: 0x4adb15)
        15:         return i, s
        16: }
        17:
        18: func sayHi() (x string, y string) {
        19:         x = "hello"
    =>  20:         y = "world"
        21:         return
        22: }
        23:
        24: func main() {
        25:         str1, str2 := sayHi()
    (dlv) p x
    "hello"
    

    运行到第一个断点处,打印x

    (dlv) clear 2
    Breakpoint 2 cleared at 0x4adb15 for main.sayHi() ./func.go:20
    

    清除第2个断点

    注意:程序执行完后,如果想再次开始调试,要先执行restart(r)。

    第17章使用命令行程序

    17.1 操作输入和输出

    名称 代码 描述
    标准输入 0 标准输入是提供给命令行程序的数据,它可以是文件,也可以是文本字符串。
    标准输出 1 包含显示到屏幕上的输出
    标准错误 2 标准错误是来自程序的错误,包含显示到屏幕上的错误消息

    17.2 访问命令行参数

    在Go语言中,要读取传递给命令行程序的参数,可使用标准库中的os包。
    os.go

    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        for i,arg := range os.Args {
            fmt.Println("argument", i, "is", arg);
        }
    }
    

    方法Args返回一个字符串切片,其中包含程序的名称以及传递给程序的所有参数。i是参数的序号,arg为是参数的值。
    执行

    go build os.go 
    ./os a1 b2 c3  
    

    结果

    argument 0 is ./os
    argument 1 is a1
    argument 2 is b2
    argument 3 is c3
    

    17.3 分析命令行标志

    虽然可使用os包来获取命令行参数,但Go语言还在标准库中提供了flag包。除os.Args的功能外,这个包还提供了众多其他的功能,其中包括以下几点。

    • 指定作为参数传递的值的类型。
    • 设置标志的默认值。
    • 自动生成帮助文本。

    下面的程序演示了flag包的用法。
    flag.go

    package main
    
    import (
        "fmt"
        "flag"
    )
    
    func main() {
        s := flag.String("s", "Hello world", "String help text")
        flag.Parse()
    
        fmt.Println("value of s:", *s)
    }
    
    go run flag.go -s haha
    value of s: haha
    

    对这个程序解读如下。

    • 声明变量s并将其设置为flag.String返回的值。
    • flag.String能够让您声明命令行标志,并指定其名称、默认值和帮助文本。
    • 调用flag.Parse,让程序能够传递声明的参数。
    • 最后,打印变量s的值。请注意,flag.String返回的是一个指针,因此使用运算符*对其解除引用,以便显示底层的值。

    flag包会自动创建一些帮助文本,要显示它们,可使用如下任何标志。

    • -h
    • --h
    • -help
    • --help
    go run flag.go -h
    Usage of /tmp/go-build350295438/b001/exe/flag:
      -s string
            String help text (default "Hello world")
    exit status 2
    

    17.4 指定标志的类型

    flag包根据声明分析标志的类型,这对应于Go语言的类型系统。编写命令行程序时,必须考虑程序将接受的数据,并将其映射到正确的类型,这一点很重要。下例演示了如何分析String、Int和Boolean标志,并将它们的值打印到终端。

    flag2.go

    package main
    
    import (
        "fmt"
        "flag"
    )
    
    func main() {
        s := flag.String("s", "Hello world", "String help text")
        i := flag.Int("i", 0, "Int help text")
        b := flag.Bool("b", false, "Bool help text")
        flag.Parse()
    
        fmt.Println("value of s:", *s)
        fmt.Println("value of i:", *i)
        fmt.Println("value of b:", *b)
    }
    
    go run flag2.go -i 100 -b 
    value of s: Hello world
    value of i: 100
    value of b: true
    

    请注意,对于Boolean标志,如果仅指定它,将把它的值设置为true。

    当输入类型错误时会有提示

    go run flag2.go -i hello
    invalid value "hello" for flag -i: parse error
    Usage of /tmp/go-build329274630/b001/exe/flag2:
      -b    Bool help text
      -i int
            Int help text
      -s string
            String help text (default "Hello world")
    exit status 2
    

    17.5 自定义帮助文本

    虽然flag包会自动生成帮助文本,但完全可以覆盖默认的帮助格式并提供自定义的帮助文本。为此可将变量Usage设置为一个函数,这样每当在分析标志的过程中发生错误或使用-h获取帮助时,都将调用这个函数。下面是这个函数的一种简单实现。

    flag.Usage = func(){
            text := "this is myself help"
    
            fmt.Fprintf(os.Stderr, "%s\n", text)
    }
    

    17.8 安装和分享命令行程序

    开发好命令行程序后,请在您的系统中安装它,以便能够在任何地方,而不是只能在命令gobuild生成的二进制文件所在的文件夹中才能访问它。要让Go工具发挥作用,必须遵循Go语言约定,这很重要。为此,必须正确地设置$GOPATH。

    遵循Go语言的约定在于,您现在可以将代码提交到Github,让别人能够使用下面的命令轻松地安装它。

    go get github.com/[your github username]/helloworld
    

    17.11 作业

    请阐述go get和go install之间的差别。

    go install用于安装本地包,这可能是您编写的文件,也可能是您从网上或文件服务器中下载的文件。go install从远程服务器(如Github)获取文件,并像go install那样安装它们。这两个命令的作用大致相同,它们都安装文件,但go get还下载文件。

    第18章创建HTTP服务器

    18.1 通过Hello World Web服务器宣告您的存在

    标准库中的net/http包提供了多种创建HTTP服务器的方法,它还提供了一个基本路由器。

    package main
    
    import (
        "net/http"
    )
    
    func helloWorld(w http.ResponseWriter, r *http.Request){
        w.Write([]byte("Hello World\n"))
    }
    
    func main(){
        http.HandleFunc("/", helloWorld)
        http.ListenAndServe(":8000", nil)
    }  
    

    运行这个程序,然后执行

    curl "http://127.0.0.1:8000"
    

    可以看到Hello World的结果。

    说明:

    • 导入net/http包。
    • 在main函数中,使用方法HandleFunc创建了路由/。这个方法接受一个模式和一个函数,其中前者描述了路径,而后者指定如何对发送到该路径的请求做出响应。
    • 函数helloWorld接受一个http.ResponseWriter和一个指向请求的指针。这意味着在这个函数中,可查看或操作请求,再将响应返回给客户端。在这里,使用了方法Write来生成响应。这个方法生成的HTTP响应包含状态、报头和响应体。[ ]byte声明一个字节切片并将字符串值转换为字节。这意味着方法Write可以使用[ ]byte,因为这个方法将一个字节切片作为参数。
    • 为响应客户端,使用了方法ListenAndServe来启动一个服务器,这个服务器监听localhost和端口8000。

    18.2 查看请求和响应

    18.2.2 详谈路由

    HandleFunc用于注册对URL地址映射进行响应的函数。简单地说,HandleFunc创建一个路由表,让HTTP服务器能够正确地做出响应。

    在这个示例中,每当用户向 / 发出请求时,都将调用函数helloWorld,每当用户向 /users/发出请求时,都将调用函数usersHandler,依此类推。

    http.HandleFunc("/", helloWorld)
    http.HandleFunc("/users/", usersHandler)
    http.HandleFunc("/projects/", projectsHandler)
    

    有关路由器的行为,有以下几点需要注意。

    • 路由器默认将没有指定处理程序的请求定向到 /。
    • 路由必须完全匹配。例如,对于向 /users发出的请求,将定向到 /,因为这里末尾少了斜杆。
    • 路由器不关心请求的类型,而只管将与路由匹配的请求传递给相应的处理程序。

    18.3 使用处理程序函数

    在Go语言中,路由器负责将路由映射到函数,但如何处理请求以及如何向客户端返回响应,是由处理程序函数定义的。很多编程语言和Web框架都采用这样的模式,即先由函数来处理请求和响应,再返回响应。在这方面,Go语言也如此。处理程序函数负责完成如下常见任务。

    • 读写报头。
    • 查看请求的类型。
    • 从数据库中取回数据。
    • 分析请求数据。
    • 验证身份。

    处理程序函数能够访问请求和响应,因此一种常见的模式是,先完成对请求的所有处理,再将响应返回给客户端。响应生成后,就不能再对其做进一步的处理了。比如http的响应头必须在响应之前发送,不然就没有意义了。

    18.4 处理404错误

    然而,鉴于请求的路由不存在,原本应返回404错误(页面未找到)。为此,可在处理默认路由的函数中检查路径,如果路径不为 /,就返回404错误,程序示例如下。

    package main
    
    import (
        "net/http"
    )
    
    func helloWorld(w http.ResponseWriter, r *http.Request){
        if r.URL.Path != "/"{
            http.NotFound(w, r)
            return
        }
    
        w.Write([]byte("Hello World\n"))
    }
    
    func main(){
        http.HandleFunc("/", helloWorld)
        http.ListenAndServe(":8000", nil)
    }
    

    相比于原来的Hello World Web服务器,这里所做的修改如下。

    • 在处理程序函数helloWorld中,检查路径是否是 /。
    • 如果不是,就调用http包中的方法NotFound,并将响应和请求传递给它。这将向客户端返回一个404响应。
    • 如果路径与 / 匹配,则if语句将被忽略,进而发送响应Hello World。

    18.5 设置报头

    创建HTTP服务器时,经常需要设置响应的报头。在创建、读取、更新和删除报头方面,Go语言提供了强大的支持。在下面的示例中,假设服务器将发送一些JSON数据。通过设置Content-Type报头,服务器可告诉客户端,发送的是JSON数据。处理程序函数可使用ResponseWriter来添加报头,如下所示。

    package main
    
    import (
        "net/http"
    )
    
    func helloWorld(w http.ResponseWriter, r *http.Request){
        if r.URL.Path != "/"{
            http.NotFound(w, r)
            return
        }
        
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        w.Write([]byte(`{"hello":"world"}`))
    }
    
    func main(){
        http.HandleFunc("/", helloWorld)
        http.ListenAndServe(":8000", nil)
    }
    

    执行及相应结果

    curl -is "http://127.0.0.1:8000/" 
    
    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8
    Date: Thu, 02 Jul 2020 07:07:12 GMT
    Content-Length: 17
    
    {"hello":"world"}
    

    18.6 响应以不同类型的内容

    响应客户端时,HTTP服务器通常提供多种类型的内容。一些常用的内容类型包括text/plain、text/html、application/json和application/xml。如果服务器支持多种类型的内容,客户端可使用Accept报头请求特定类型的内容。这意味着同一个URL可能向浏览器提供HTML,而向API客户端提供JSON。只需对本章的示例稍作修改,就可让它查看客户端发送的Accept报头,并据此提供不同类型的内容,如程序如下。

    func helloWorld(w http.ResponseWriter, r *http.Request){
        if r.URL.Path != "/"{
            http.NotFound(w, r)
            return
        }   
            
        switch r.Header.Get("Accept"){
            case "application/json":
            //your output
            case "application/xml":
            //your output
            default:
            //your output
        }   
    }
    

    核心在于了解r.Header.Get()可以取到request header中的字段。

    18.7 响应不同类型的请求

    除响应以不同类型的内容外,HTTP服务器通常也需要能够响应不同类型的请求。客户端可发出的请求类型是HTTP规范中定义的,包括GET、POST、PUT和DELETE。要使用Go语言创建能够响应不同类型请求的HTTP服务器,可采用类似于提供多种类型内容的方法,下例所示。

    package main
    
    import (
        "net/http"
    )
    
    func helloWorld(w http.ResponseWriter, r *http.Request){
        if r.URL.Path != "/"{
            http.NotFound(w, r)
            return
        }
            
        switch r.Method{
            case "GET":
                w.Write([]byte("Recv a GET request"))
            case "POST":
                w.Write([]byte("Recv a POST request"))
            default:
                w.Write([]byte("What's this"))
        }
    
    }
    
    func main(){
        http.HandleFunc("/", helloWorld)
        http.ListenAndServe(":8000", nil)
    }
    

    测试

    curl -X POST "http://127.0.0.1:8000/" 
    Recv a POST request
    

    18.8 获取GET和POST请求中的数据

    package main
    
    import (
        "net/http"
        "fmt"
        "io/ioutil"
        "log"
    )
    
    func helloWorld(w http.ResponseWriter, r *http.Request){
        if r.URL.Path != "/"{
            http.NotFound(w, r)
            return
        }
            
        switch r.Method{
            case "GET":
                for k, v := range r.URL.Query(){
                    fmt.Printf("%s: %s\n", k, v)
                }
                w.Write([]byte("Recv a GET request"))
            case "POST":
                reqBody, err := ioutil.ReadAll(r.Body)
                if err != nil {
                    log.Fatal(err)
                }
                
                fmt.Printf("%s\n", reqBody)
    
                w.Write([]byte("Recv a POST request"))
            default:
                w.Write([]byte("What's this"))
        }
    
    }
    
    func main(){
        http.HandleFunc("/", helloWorld)
        http.ListenAndServe(":8000", nil)
    }
    

    说明:

    • 在Go语言中,以字符串映射的方式提供了请求中的查询字符串参数,您可使用range子句来遍历它们。
    for k, v := range r.URL.Query(){
        fmt.Printf("%s: %s\n", k, v)
    }
    
    • 在POST请求中,数据通常是在请求体中发送的。要读取并使用这些数据,可像下面这样做。
    reqBody, err := ioutil.ReadAll(r.Body)
    if err != nil {
        log.Fatal(err)
    }
    

    相关文章

      网友评论

        本文标题:《Go语言入门经典》16~18章读书笔记

        本文链接:https://www.haomeiwen.com/subject/cdyrqktx.html