Go打印結構體提升代碼調試效率實例詳解
引言
不知道大家是否遇到打印復雜結構的需求?
結構體的特點是有點像是一個盒子,但不同于 slice 與 map,它里面可以放很多不同類型的東西,如數(shù)字、字符串、slice、map 或者其他結構體。
但,如果我們想看看盒子中內(nèi)容,該怎么辦呢?這時我們就要能打印結構體了。
打印結構體的能力其實挺重要的,它能幫我們檢查理解代碼,提高調試效率,確保代碼運行正確。
本文讓我們以此為話題,聊聊 GO 語言中如何打印結構體。這些方法,基本上同樣適用于其他復雜數(shù)據(jù)結構
讓我們直接開始吧!
定義結構體
首先,我們來定義一個結構體,它會被接下來所有的方法用到。
代碼如下所示:
type Author struct { Name string Age int8 Sex string } type Article struct { ID int64 Title string Author *Author Content string }
我們定義了一個 Article
結構體用于表示一篇文章,內(nèi)部包含內(nèi)部實現(xiàn)了 ID
、Title
和 Content
基礎屬性,還有一個字段是 Author
結構體指針,用于保存作者信息。
我將先介紹四種基本的方式。
使用 fmt.Printf
最簡單的方法是使用 fmt.Printf
函數(shù),如果希望顯示一些詳情,可和 %+v
格式化符號配合。這樣可以直接打印出結構體的字段名和值。
我們可以這樣打印它,代碼如下:
func main() { article := Article{ ID: 1, Title: "How to Print a Structure in Golang", Author: &Author{"poloxue", 18, "male"}, Content: "This is a blog post", } fmt.Printf("%+v\n", article) }
輸出:
{ID:1 Title:How to Print a Structure in Golang Author:0xc0000900c0 Content:This is a blog post}
如上所示,這段代碼會打印出 article
結構體的所有字段值。不過,如果仔細觀察,會發(fā)現(xiàn)它的 Author
字段只打印了指針地址 - Author:0xc0000900c0
,沒有輸出它的內(nèi)容。
這其實是符合預期的。*Author
是指針類型,它的值自然就是地址。
如果我就想打印 Author
字段的內(nèi)容,可通過用 fmt.Printf
打印指針實際指向內(nèi)容。
fmt.Print("%+v\n", article.Author)
輸出:
&{Name:poloxue Age:18 Sex:male}
我在測試的時候,發(fā)現(xiàn)個有趣的現(xiàn)象:
如果打印的是結構體指針,它會自動解引用,即能把內(nèi)容打印出來。如上的代碼所示,無論是
Printf("%+v\n", article.Author)
還是
Printf("%\n", *article.Author)
都能打印出結構體的內(nèi)容。
但如果打印的結構體,包含結構體指針字段,則不會將內(nèi)容打印出來,而只會打印地址,即指針值。
我猜測,如此設計的原因是為了防止深層遞歸,或者循環(huán)引用。
想明白的話,似乎是個顯而易見的事情。
實現(xiàn) String 方法
除了以上將 Author
字段單獨拿出打印,我們還有其他方法實現(xiàn)嗎?當然有,這就是本節(jié)要說的 - 實現(xiàn) String
方法。
這其實是 Go 提供的一種機制,一個類型如果滿足 Stringer
接口,即實現(xiàn)了 String
方法,打印時返回的就是 String
方法的返回內(nèi)容。
Stringer
定義如下:
type Stringer interface { String() string }
當我們使用 fmt.Printf
打印結構體時,就會調用定義的 String
方法,控制結構體的輸出格式。例如:
func (a Article) String() string { return fmt.Sprintf("Title: %s, Author: %s, Content: %s", a.Title, a.Author.Name, a.Content) } func main() { article := Article{ ID: 1, Title: "How to Print a Structure in Golang", Author: &Author{"poloxue", 18, "male"}, Content: "This is a blog post", } fmt.Println(article) }
輸出:
Title: How to Print a Structure in Golang, Author: poloxue, Content: This is a blog post
檢查結果,的確是 String
方法中定義的形式?,F(xiàn)在,我們可以隨心所欲定義打印格式了。
這種方式還有一個特點,就是性能高。畢竟,它沒有任何啰嗦,直接到拿到結果。
到這里,我們已經(jīng)有能力打印結構體了。但這里也有些缺點。
首先,不夠美觀,輸出結構易讀性差。這不利于快速定位。
其次,每次都要自定義輸出。如果只是為了 debug 調試代碼,而不是功能代碼,希望有更方便方式直接打印出所有內(nèi)容。
json.MarshalIndent
首先,如何實現(xiàn)美化輸出呢?
如果你想要一個更美觀的輸出格式,最便捷的方式,可使用標準庫 encoding/json 的 json.MarshalIndent
函數(shù),它會將結構體轉換為 JSON 格式,且能控制縮進,使輸出更易于閱讀。
示例代碼,如下所示:
import ( "encoding/json" "fmt" ) func main() { article := Article{ ID: 1, Title: "How to Print a Structure in Golang", Author: &Author{"poloxue", 18, "male"}, Content: "This is a blog post", } articleJSON, _ := json.MarshalIndent(article, "", " ") fmt.Println(string(articleJSON)) }
如上的代碼中,json.MarshalIndent
的第三個參數(shù)表示縮進的大小。我們看下代碼的輸出吧。
輸出:
{
"ID": 1,
"Title": "How to Print a Structure in Golang",
"Author": {
"Name": "poloxue",
"Age": 18,
"Sex": "male"
},
"Content": "This is a blog post"
}
以這樣美觀的 JSON 格式打印的 Article
結構體,明顯易讀了許多。
reflect 包打印復雜結構
如果想完全控制結構體打印,還可使用 reflect
包。它不僅僅是可以拿到 Go 變量的值,其他信息,如結構體的字段名和類型,都可輕而易舉拿到。
這也是為什么可通過 reflect
包能最大粒度控制輸出格式。
示例代碼,如下所示:
import ( "fmt" "reflect" ) func main() { article := Article{ ID: 1, Title: "How to Print a Structure in Golang", Author: &Author{"poloxue", 18, "male"}, Content: "This is a blog post", } val := reflect.ValueOf(article) for i := 0; i < val.NumField(); i++ { field := val.Type().Field(i) fmt.Printf( "Type: %v, Field: %s, Value: %v\n", field.Type, field.Name, val.Field(i), ) } }
輸出:
Type: int64, Field: ID, Value: 1
Type: string, Field: Title, Value: How to Print a Structure in Golang
Type: *main.Author, Field: Author, Value: &{poloxue 18 male}
Type: string, Field: Content, Value: This is a blog post
我們輸出了字段類型、名稱和值。
當然,reflect
提供了靈活性,但具體的打印格式,我們就要自己按需求自行定義。前面介紹的 Printf
函數(shù),內(nèi)部實現(xiàn)本質上也依賴了 reflect
。
如果想要深度打印信息,即使是指針類型字段,也可通過 reflect
繼續(xù)深度打印。
代碼類似于:
if field.Type.Kind() == reflect.Pointer { fmt.Println(val.Field(i).Elem()) }
即如果是指針類型,通過 Elem()
繼續(xù)深入它的內(nèi)部。
當然,如果你希望得到結構體類型相關信息。reflect
甚至可以在結構體沒有實例化打印其類型的詳情。
func printStructType(t reflect.Type) { for i := 0; i < t.NumField(); i++ { field := t.Field(i) fmt.Printf("%s: %s\n", field.Name, field.Type) } } func main() { t := reflect.TypeOf((*Article)(nil)).Elem() printStructType(t) }
核心就是那句 (*Article)(nil)
得到一個類型為 *Article
的 nil
。也算是類型內(nèi)存空間的占用。
性能壓測
我嘗試了不同的打印方法后,也進行了一個簡單的性能測試。
測試結果如下所示:
BenchmarkFmtPrintf-16 2631248 447.3 ns/op
BenchmarkJSONMarshalIndent-16 997448 1016 ns/op
BenchmarkCustomStringMethod-16 5135541 225.5 ns/op
BenchmarkReflection-16 2030233 594.9 ns/op
測試結果顯示,使用自定義的 String
方法是最快的,而 json.MarshalIndent
則是最慢的。
這意味著如果你關心程序的運行速度,最好使用自定義的String
方法來打印結構體。
這里單獨提醒一點,因為 fmt.Printf
的內(nèi)部是使用反射,所以要能測試出 String()
自定義的效果,內(nèi)部實現(xiàn)就不要用 fmt.Sprintf
等方法格式化字符,而是推薦使用 strconv
中的一些函數(shù)。
示例代碼:
func (a Article) String() string { return "{ID:" + strconv.Itoa(int(a.ID)) + ", Title:" + a.Title + ",AuthorName:" + a.Author.Name + "}" }
這樣才能真正意義上測試出 String
自定義的優(yōu)勢。不靠套娃,最終得到用了 String
等于沒用的效果。
如果想知道為什么 strconv
更快,可閱讀我之前的一篇文章:GO 中高效 int 轉換 string 的方法與源碼剖析。
三方庫
前面介紹了 4 種打印結構內(nèi)容的方案,。特別是第四種,提供了最大化的自由度。但缺點是要自定義,非常麻煩。
接下來,我嘗試推薦一些好用的三方庫,它們將我們常用的一些模式實踐成庫,便于我們使用。
go-spew
我們首先來看看一個叫做 go-spew
的第三方庫。
這個庫提供了深度打印 Go 數(shù)據(jù)結構的功能,對于調試非常有用。它可以遞歸地打印出結構體的字段,即使是嵌套的結構體也能打印出來。
例如:
import "github.com/davecgh/go-spew/spew" func main() { article := Article{ ID: 1, Title: "How to Print a Structure in Golang", Author: &Author{"poloxue", 18, "male"}, Content: "This is a blog post", } spew.Dump(article) }
這樣會詳細地打印出 article
結構體的所有內(nèi)容。
輸出如下:
(main.Article) {
ID: (int64) 1,
Title: (string) (len=34) "How to Print a Structure in Golang",
Author: (*main.Author)(0xc000100330)({
Name: (string) (len=7) "poloxue",
Age: (int8) 18,
Sex: (string) (len=4) "male"
}),
Content: (string) (len=19) "This is a blog post"
可以看出,上面的輸出內(nèi)容包含的信息非常豐富。
如果希望自定義打印格式,可通過 spew
提供的 ConfigState
配置,如縮進,打印深度。
示例代碼:
// 設置 spew 的配置 spewConfig := spew.ConfigState{ Indent: "\t", // 索引為 Tab DisableMethods: true, DisablePointerMethods: true, DisablePointerAddresses: true, MaxDepth: 1, // 設置打印深度為 1 } spewConfig.Dump(article)
輸出:
(main.Article) {
ID: (int64) 1,
Title: (string) (len=34) "How to Print a Structure in Golang",
Author: (*main.Author)({
<max depth reached>
}),
Content: (string) (len=19) "This is a blog post"
}
因為,我將打印深度配置為 1,可以看到 Author
的字段的內(nèi)容是沒有打印的。
更多能力可自行探索。
pretty
除了 go-spew
,還有一些沒那么強大,但也還不錯的庫,方便我們調試復雜數(shù)據(jù)結構,如 pretty
[1]。
import ( "fmt" "github.com/kr/pretty" ) func main() { // 省略 ... fmt.Printf("%# v\n", pretty.Formatter(article)) }
輸出:main.Article{
ID: 1,
Title: "How to Print a Structure in Golang",
Author: &main.Author{Name:"poloxue", Age:18, Sex:"male"},
Content: "This is a blog post",
}
輸出結果為格式化的結構體輸出。
這兩個庫都已經(jīng)處于很久不更新的狀態(tài),但是功能滿足我們的需求。
結語
本文主要介紹了 Go 語言中打印結構體的不同方法。我們從簡單的 fmt.Printf
到使用反射,甚至是第三方庫,我們是有很多選擇。
簡單主題深入起來,擴展內(nèi)容也可很豐富。
關于打印結構體這個主題,還有一個部分沒有談到,就是日志如何記錄類似結構體等復雜結構類型的變量,畢竟日志對于問題調試至關重要。后面有機會,可單獨談下這個主題。
最后,希望這篇文章能幫助你在打印調試 GO 結構體等復雜結構時,不再迷茫。
以上就是Go打印結構體提升代碼調試效率實例詳解的詳細內(nèi)容,更多關于Go打印結構體的資料請關注腳本之家其它相關文章!
相關文章
GO項目實戰(zhàn)之Gorm格式化時間字段實現(xiàn)
GORM自帶的time.Time類型JSON默認輸出RFC3339Nano格式的,下面這篇文章主要給大家介紹了關于GO項目實戰(zhàn)之Gorm格式化時間字段實現(xiàn)的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2023-01-01go-zero創(chuàng)建RESTful API 服務的方法
文章介紹了如何使用go-zero框架和goctl工具快速創(chuàng)建RESTfulAPI服務,通過定義.api文件并使用goctl命令,可以自動生成項目結構、路由、請求和響應模型以及處理邏輯,感興趣的朋友一起看看吧2024-11-11go中的參數(shù)傳遞是值傳遞還是引用傳遞的實現(xiàn)
參數(shù)傳遞機制是一個重要的概念,它決定了函數(shù)內(nèi)部對參數(shù)的修改是否會影響到原始數(shù)據(jù),本文主要介紹了go中的參數(shù)傳遞是值傳遞還是引用傳遞的實現(xiàn),感興趣的可以了解一下2024-12-12