第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)
}
网友评论