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

    技术2022-07-20  72

    第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) }
    Processed: 0.014, SQL: 9