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