在Go語言中有一些調試技巧能幫助我們快速找到問題,有時候你想盡可能多的記錄異常但仍覺得不夠,搞清楚堆棧的意義有助于定位Bug或者記錄更完整的信息。
“專業(yè)、務實、高效、創(chuàng)新、把客戶的事當成自己的事”是我們每一個人一直以來堅持追求的企業(yè)文化。 成都創(chuàng)新互聯(lián)是您可以信賴的網站建設服務商、專業(yè)的互聯(lián)網服務提供商! 專注于成都網站制作、做網站、外貿營銷網站建設、軟件開發(fā)、設計服務業(yè)務。我們始終堅持以客戶需求為導向,結合用戶體驗與視覺傳達,提供有針對性的項目解決方案,提供專業(yè)性的建議,創(chuàng)新互聯(lián)建站將不斷地超越自我,追逐市場,引領市場!
本文將討論堆棧跟蹤信息以及如何在堆棧中識別函數(shù)所傳遞的參數(shù)。
Functions
先從這段代碼開始:
Listing 1
01 package main
02
03 func main() {
04 ? ? slice := make([]string, 2, 4)
05 ? ? Example(slice, "hello", 10)
06 }
07
08 func Example(slice []string, str string, i int) {
09 ? ? panic("Want stack trace")
10 }
Example函數(shù)定義了3個參數(shù),1個string類型的slice, 1個string和1個integer, 并且拋出了panic,運行這段代碼可以看到這樣的結果:
Listing 2
Panic: Want stack trace
goroutine 1 [running]:
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:9 +0x64
main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:5 +0x85
goroutine 2 [runnable]:
runtime.forcegchelper()
/Users/bill/go/src/runtime/proc.go:90
runtime.goexit()
/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1
goroutine 3 [runnable]:
runtime.bgsweep()
/Users/bill/go/src/runtime/mgc0.go:82
runtime.goexit()
/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1
堆棧信息中顯示了在panic拋出這個時間所有的goroutines狀態(tài),發(fā)生的panic的goroutine會顯示在最上面。
Listing 3
01 goroutine 1 [running]:
02 main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:9 +0x64
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:5 +0x85
第1行顯示最先發(fā)出panic的是goroutine 1, 第二行顯示panic位于main.Example中, 并能定位到該行代碼,在本例中第9行引發(fā)了panic。
下面我們關注參數(shù)是如何傳遞的:
Listing 4
// Declaration
main.Example(slice []string, str string, i int)
// Call to Example by main.
slice := make([]string, 2, 4)
Example(slice, "hello", 10)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
這里展示了在main中帶參數(shù)調用Example函數(shù)時的堆棧信息,比較就能發(fā)現(xiàn)兩者的參數(shù)數(shù)量并不相同,Example定義了3個參數(shù),堆棧中顯示了6個參數(shù)?,F(xiàn)在的關鍵問題是我們要弄清楚它們是如何匹配的。
第1個參數(shù)是string類型的slice,我們知道在Go語言中slice是引用類型,即slice變量結構會包含三個部分:指針、長度(Lengthe)、容量(Capacity)
Listing 5
// Slice parameter value
slice := make([]string, 2, 4)
// Slice header values
Pointer: ?0x2080c3f50
Length: ? 0x2
Capacity: 0x4
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
因此,前面3個參數(shù)會匹配slice, 如下圖所示:
Figure 1
figure provided by Georgi Knox
我們現(xiàn)在來看第二個參數(shù),它是string類型,string類型也是引用類型,它包括兩部分:指針、長度。
Listing 6
// String parameter value
"hello"
// String header values
Pointer: 0x425c0
Length: ?0x5
// Declaration
main.Example(slice []string,?str string, i int)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4,?0x425c0, 0x5, 0xa)
可以確定,堆棧信息中第4、5兩個參數(shù)對應代碼中的string參數(shù),如下圖所示:
Figure 2
figure provided by Georgi Knox
最后一個參數(shù)integer是single word值。
Listing 7
// Integer parameter value
10
// Integer value
Base 16: 0xa
// Declaration
main.Example(slice []string, str string,?i int)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5,?0xa)
現(xiàn)在我們可以匹配代碼中的參數(shù)到堆棧信息了。
Figure 3
figure provided by Georgi Knox
Methods
如果我們將Example作為結構體的方法會怎么樣呢?
Listing 8
01 package main
02
03 import "fmt"
04
05 type trace struct{}
06
07 func main() {
08 ? ? slice := make([]string, 2, 4)
09
10 ? ? var t trace
11 ? ? t.Example(slice, "hello", 10)
12 }
13
14 func (t *trace) Example(slice []string, str string, i int) {
15 ? ? fmt.Printf("Receiver Address: %p\n", t)
16 ? ? panic("Want stack trace")
17 }
如上所示修改代碼,將Example定義為trace的方法,并通過trace的實例t來調用Example。
再次運行程序,會發(fā)現(xiàn)堆棧信息有一點不同:
Listing 9
Receiver Address:?0x1553a8
panic: Want stack trace
01 goroutine 1 [running]:
02 main.(*trace).Example(0x1553a8, 0x2081b7f50, 0x2, 0x4, 0xdc1d0, 0x5, 0xa)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:16 +0x116
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:11 +0xae
首先注意第2行的方法調用使用了pointer receiver,在package名字和方法名之間多出了"*trace"字樣。另外,參數(shù)列表的第1個參數(shù)標明了結構體(t)地址。我們從堆棧信息中看到了內部實現(xiàn)細節(jié)。
Packing
如果有多個參數(shù)可以填充到一個single word, 則這些參數(shù)值會合并打包:
Listing 10
01 package main
02
03 func main() {
04 ? ? Example(true, false, true, 25)
05 }
06?
07 func Example(b1, b2, b3 bool, i uint8) {
08 ? ? panic("Want stack trace")
09 }
這個例子修改Example函數(shù)為4個參數(shù):3個bool型和1個八位無符號整型。bool值也是用8個bit表示,所以在32位和64位架構下,4個參數(shù)可以合并為一個single word。
Listing 11
01 goroutine 1 [running]:
02 main.Example(0x19010001)
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:8 +0x64
03 main.main()
/Users/bill/Spaces/Go/Projects/src/github.com/goinaction/code/
temp/main.go:4 +0x32
這是本例的堆棧信息,看下圖的具體分析:
Listing 12
// Parameter values
true, false, true, 25
// Word value
Bits ? ?Binary ? ? ?Hex ? Value
00-07 ? 0000 0001 ??01? ??true
08-15 ? 0000 0000 ??00? ? false
16-23 ? 0000 0001 ??01? ? true
24-31 ? 0001 1001 ??19? ? 25
// Declaration
main.Example(b1, b2, b3 bool, i uint8)
// Stack trace
main.Example(0x19010001)
以上展示了參數(shù)值是如何匹配到4個參數(shù)的。當我們看到堆棧信息中包括十六進制值,需要知道這些值是如何傳遞的。
英文原文鏈接【Go, the unwritten parts】 發(fā)表于2017/05/22 作者JBD是Go語言開發(fā)小組成員
檢查程序的執(zhí)行路徑和當前狀態(tài)是非常有用的調試手段。核心文件(core file)包含了一個運行進程的內存轉儲和狀態(tài)。它主要是用來作為事后調試程序用的。它也可以被用來查看一個運行中的程序的狀態(tài)。這兩個使用場景使調試文件轉儲成為一個非常好的診斷手段。我們可以用這個方法來做事后診斷和分析線上的服務(production services)。
在這篇文章中,我們將用一個簡單的hello world網站服務作為例子。在現(xiàn)實中,我們的程序很容易就會變得很復雜。分析核心轉儲給我們提供了一個機會去重構程序的狀態(tài)并且查看只有在某些條件/環(huán)境下才能重現(xiàn)的案例。
作者注 : 這個調試流程只在Linux上可行。我不是很確定它是否在其它Unixs系統(tǒng)上工作。macOS對此還不支持。Windows現(xiàn)在也不支持。
在我們開始前,需要確保核心轉儲的ulimit設置在合適的范圍。它的缺省值是0,意味著最大的核心文件大小是0。我通常在我的開發(fā)機器上將它設置成unlimited。使用以下命令:
接下來,你需要在你的機器上安裝 delve 。
下面我們使用的 main.go 文件。它注冊了一個簡單的請求處理函數(shù)(handler)然后啟動了HTTP服務。
讓我們編譯并生產二進制文件。
現(xiàn)在讓我們假設,這個服務器出了些問題,但是我們并不是很確定問題的根源。你可能已經在程序里加了很多輔助信息,但還是無法從這些調試信息中找出線索。通常在這種情況下,當前進程的快照會非常有用。我們可以用這個快照深入查看程序的當前狀態(tài)。
有幾個方式來獲取核心文件。你可能已經熟悉了奔潰轉儲(crash dumps)。它們是在一個程序奔潰的時候寫入磁盤的核心轉儲。Go語言在缺省設置下不會生產奔潰轉儲。但是當你把 GOTRACEBACK 環(huán)境變量設置成“crash”,你就可以用 Ctrl+backslash 才觸發(fā)奔潰轉儲。如下圖所示:
上面的操作會使程序終止,將堆棧跟蹤(stack trace)打印出來,并把核心轉儲文件寫入磁盤。
另外個方法可以從一個運行的程序獲得核心轉儲而不需要終止相應的進程。 gcore 可以生產核心文件而無需使運行中的程序退出。
根據(jù)上面的操作,我們獲得了轉儲而沒有終止對應的進程。下一步就是把核心文件加載進delve并開始分析。
差不多就這些。delve的常用操作都可以使用。你可以backtrace,list,查看變量等等。有些功能不可用因為我們使用的核心轉儲是一個快照而不是正在運行的進程。但是程序執(zhí)行路徑和狀態(tài)全部可以訪問。
Go 由于不支持泛型而臭名昭著,但最近,泛型已接近成為現(xiàn)實。Go 團隊實施了一個看起來比較穩(wěn)定的設計草案,并且正以源到源翻譯器原型的形式獲得關注。本文講述的是泛型的最新設計,以及如何自己嘗試泛型。
例子
FIFO Stack
假設你要創(chuàng)建一個先進先出堆棧。沒有泛型,你可能會這樣實現(xiàn):
type?Stack?[]interface{}func?(s?Stack)?Peek()?interface{}?{
return?s[len(s)-1]
}
func?(s?*Stack)?Pop()?{
*s?=?(*s)[:
len(*s)-1]
}
func?(s?*Stack)?Push(value?interface{})?{
*s?=?
append(*s,?value)
}
但是,這里存在一個問題:每當你 Peek 項時,都必須使用類型斷言將其從 interface{} 轉換為你需要的類型。如果你的堆棧是 *MyObject 的堆棧,則意味著很多 s.Peek().(*MyObject)這樣的代碼。這不僅讓人眼花繚亂,而且還可能引發(fā)錯誤。比如忘記 * 怎么辦?或者如果您輸入錯誤的類型怎么辦?s.Push(MyObject{})` 可以順利編譯,而且你可能不會發(fā)現(xiàn)到自己的錯誤,直到它影響到你的整個服務為止。
通常,使用 interface{} 是相對危險的。使用更多受限制的類型總是更安全,因為可以在編譯時而不是運行時發(fā)現(xiàn)問題。
泛型通過允許類型具有類型參數(shù)來解決此問題:
type?Stack(type?T)?[]Tfunc?(s?Stack(T))?Peek()?T?{
return?s[len(s)-1]
}
func?(s?*Stack(T))?Pop()?{
*s?=?(*s)[:
len(*s)-1]
}
func?(s?*Stack(T))?Push(value?T)?{
*s?=?
append(*s,?value)
}
這會向 Stack 添加一個類型參數(shù),從而完全不需要 interface{}?,F(xiàn)在,當你使用 Peek() 時,返回的值已經是原始類型,并且沒有機會返回錯誤的值類型。這種方式更安全,更容易使用。(譯注:就是看起來更丑陋,^-^)
此外,泛型代碼通常更易于編譯器優(yōu)化,從而獲得更好的性能(以二進制大小為代價)。如果我們對上面的非泛型代碼和泛型代碼進行基準測試,我們可以看到區(qū)別:
type?MyObject?struct?{
X?
int
}
var?sink?MyObjectfunc?BenchmarkGo1(b?*testing.B)?{
for?i?:=?0;?i??b.N;?i++?{
var?s?Stack
s.Push(MyObject{})
s.Push(MyObject{})
s.Pop()
sink?=?s.Peek().(MyObject)
}
}
func?BenchmarkGo2(b?*testing.B)?{
for?i?:=?0;?i??b.N;?i++?{
var?s?Stack(MyObject)
s.Push(MyObject{})
s.Push(MyObject{})
s.Pop()
sink?=?s.Peek()
}
}
結果:
BenchmarkGo1BenchmarkGo1-16?????12837528?????????87.0?ns/op???????48?B/op????????2?allocs/opBenchmarkGo2BenchmarkGo2-16?????28406479?????????41.9?ns/op???????24?B/op????????2?allocs/op
在這種情況下,我們分配更少的內存,同時泛型的速度是非泛型的兩倍。
合約(Contracts)
上面的堆棧示例適用于任何類型。但是,在許多情況下,你需要編寫僅適用于具有某些特征的類型的代碼。例如,你可能希望堆棧要求類型實現(xiàn) String() 函數(shù)
golang方法(method)返回值提取結構體(struct)取不到地址的原因是,①返回值并沒有保存到變量中,返回值本身只是臨時保存在程序運行的堆棧的某個不確定位置,不能取地址;②實參取地址用的操作符是是,而形參聲明變量類型為指針,需要地址值用的才是*;③聲明形參為指針的參數(shù)的實參只能為地址值。
故先把修改后的代碼列出,修改要點是把“*NewPerson1().Speak()”改為“var b=NewPerson1();(b).Speak()”,同時把“NewPerson2().Speak()”改成“var a=NewPerson2();(a).Speak()”,代碼列出如下:
package main;
import "fmt";
type PersonA struct{
name string
}
func (p *PersonA) Speak () {
fmt.Println ( "person speak" ,p.name)
}
func (p PersonA) Walk ( ){
fmt . Println ( "person walk",p.name)}
func NewPerson1()(p PersonA){
return PersonA{"new Person1"}}
func NewPerson2()(p PersonA){
return PersonA{"new Person2"}}
func main () {
var a=NewPerson2 (); (a).Speak ();?
a .Walk ();
fmt. Println ("--------------------")?;
var b=NewPerson1 ();(b).Speak ();
b.Walk ()}
go代碼調試效果
關于指針變量的使用這一點go語言和其他有指針的程序語言如c語言是一樣的,從來只有返回值為地址/指針,而從沒有在賦值前給返回值取地址這種運算,類似的錯誤晚點再整理。
不一樣的是,go語言更簡單go語言函數(shù)可以使用結構體或者結構體的指針(pointer)以傳遞結構體參數(shù),而且和c語言不一樣的是,go語言沒有區(qū)分結構體指針和結構體訪問成員的運算符,go語言只有“.”適用于兩種情況,而沒有c語言為結構體指針專門準備的“-”運算符。
可以使用結構體指針,作為結構體的方法的參數(shù)以指代自身嗎,