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