程序员

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

2020-07-02  本文已影响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

常用命令:

例:

(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

对这个程序解读如下。

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

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的结果。

说明:

18.2 查看请求和响应

18.2.2 详谈路由

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

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

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

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

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服务器,这里所做的修改如下。

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

说明:

for k, v := range r.URL.Query(){
    fmt.Printf("%s: %s\n", k, v)
}
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
    log.Fatal(err)
}
上一篇 下一篇

猜你喜欢

热点阅读