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