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

第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®。

第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)
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章