Go打印結構體提升代碼調試效率實例詳解
引言
不知道大家是否遇到打印復雜結構的需求?
結構體的特點是有點像是一個盒子,但不同于 slice 與 map,它里面可以放很多不同類型的東西,如數字、字符串、slice、map 或者其他結構體。

但,如果我們想看看盒子中內容,該怎么辦呢?這時我們就要能打印結構體了。
打印結構體的能力其實挺重要的,它能幫我們檢查理解代碼,提高調試效率,確保代碼運行正確。
本文讓我們以此為話題,聊聊 GO 語言中如何打印結構體。這些方法,基本上同樣適用于其他復雜數據結構
讓我們直接開始吧!
定義結構體
首先,我們來定義一個結構體,它會被接下來所有的方法用到。
代碼如下所示:
type Author struct {
Name string
Age int8
Sex string
}
type Article struct {
ID int64
Title string
Author *Author
Content string
}我們定義了一個 Article 結構體用于表示一篇文章,內部包含內部實現了 ID、Title 和 Content 基礎屬性,還有一個字段是 Author 結構體指針,用于保存作者信息。
我將先介紹四種基本的方式。

使用 fmt.Printf
最簡單的方法是使用 fmt.Printf 函數,如果希望顯示一些詳情,可和 %+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ā)現它的 Author 字段只打印了指針地址 - Author:0xc0000900c0 ,沒有輸出它的內容。
這其實是符合預期的。*Author 是指針類型,它的值自然就是地址。
如果我就想打印 Author 字段的內容,可通過用 fmt.Printf 打印指針實際指向內容。
fmt.Print("%+v\n", article.Author)輸出:
&{Name:poloxue Age:18 Sex:male}
我在測試的時候,發(fā)現個有趣的現象:
如果打印的是結構體指針,它會自動解引用,即能把內容打印出來。如上的代碼所示,無論是
Printf("%+v\n", article.Author)還是
Printf("%\n", *article.Author)都能打印出結構體的內容。
但如果打印的結構體,包含結構體指針字段,則不會將內容打印出來,而只會打印地址,即指針值。
我猜測,如此設計的原因是為了防止深層遞歸,或者循環(huán)引用。

想明白的話,似乎是個顯而易見的事情。
實現 String 方法
除了以上將 Author 字段單獨拿出打印,我們還有其他方法實現嗎?當然有,這就是本節(jié)要說的 - 實現 String 方法。
這其實是 Go 提供的一種機制,一個類型如果滿足 Stringer 接口,即實現了 String 方法,打印時返回的就是 String 方法的返回內容。
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 方法中定義的形式。現在,我們可以隨心所欲定義打印格式了。
這種方式還有一個特點,就是性能高。畢竟,它沒有任何啰嗦,直接到拿到結果。

到這里,我們已經有能力打印結構體了。但這里也有些缺點。
首先,不夠美觀,輸出結構易讀性差。這不利于快速定位。
其次,每次都要自定義輸出。如果只是為了 debug 調試代碼,而不是功能代碼,希望有更方便方式直接打印出所有內容。
json.MarshalIndent
首先,如何實現美化輸出呢?
如果你想要一個更美觀的輸出格式,最便捷的方式,可使用標準庫 encoding/json 的 json.MarshalIndent 函數,它會將結構體轉換為 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 的第三個參數表示縮進的大小。我們看下代碼的輸出吧。
輸出:
{
"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 函數,內部實現本質上也依賴了 reflect 。
如果想要深度打印信息,即使是指針類型字段,也可通過 reflect 繼續(xù)深度打印。
代碼類似于:
if field.Type.Kind() == reflect.Pointer {
fmt.Println(val.Field(i).Elem())
}即如果是指針類型,通過 Elem() 繼續(xù)深入它的內部。
當然,如果你希望得到結構體類型相關信息。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。也算是類型內存空間的占用。
性能壓測
我嘗試了不同的打印方法后,也進行了一個簡單的性能測試。

測試結果如下所示:
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 的內部是使用反射,所以要能測試出 String() 自定義的效果,內部實現就不要用 fmt.Sprintf 等方法格式化字符,而是推薦使用 strconv 中的一些函數。
示例代碼:
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 種打印結構內容的方案,。特別是第四種,提供了最大化的自由度。但缺點是要自定義,非常麻煩。
接下來,我嘗試推薦一些好用的三方庫,它們將我們常用的一些模式實踐成庫,便于我們使用。
go-spew
我們首先來看看一個叫做 go-spew 的第三方庫。
這個庫提供了深度打印 Go 數據結構的功能,對于調試非常有用。它可以遞歸地打印出結構體的字段,即使是嵌套的結構體也能打印出來。
例如:
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 結構體的所有內容。
輸出如下:
(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"
可以看出,上面的輸出內容包含的信息非常豐富。
如果希望自定義打印格式,可通過 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 的字段的內容是沒有打印的。
更多能力可自行探索。
pretty
除了 go-spew,還有一些沒那么強大,但也還不錯的庫,方便我們調試復雜數據結構,如 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",
}
輸出結果為格式化的結構體輸出。
這兩個庫都已經處于很久不更新的狀態(tài),但是功能滿足我們的需求。
結語
本文主要介紹了 Go 語言中打印結構體的不同方法。我們從簡單的 fmt.Printf 到使用反射,甚至是第三方庫,我們是有很多選擇。
簡單主題深入起來,擴展內容也可很豐富。
關于打印結構體這個主題,還有一個部分沒有談到,就是日志如何記錄類似結構體等復雜結構類型的變量,畢竟日志對于問題調試至關重要。后面有機會,可單獨談下這個主題。
最后,希望這篇文章能幫助你在打印調試 GO 結構體等復雜結構時,不再迷茫。
以上就是Go打印結構體提升代碼調試效率實例詳解的詳細內容,更多關于Go打印結構體的資料請關注腳本之家其它相關文章!
相關文章
go-zero創(chuàng)建RESTful API 服務的方法
文章介紹了如何使用go-zero框架和goctl工具快速創(chuàng)建RESTfulAPI服務,通過定義.api文件并使用goctl命令,可以自動生成項目結構、路由、請求和響應模型以及處理邏輯,感興趣的朋友一起看看吧2024-11-11

