Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go打印结构体

Go打印结构体提升代码调试效率实例详解

作者:波罗学 码途漫漫

这篇文章主要介绍了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 结构体用于表示一篇文章,内部包含内部实现了 IDTitle 和 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 结构体的所有字段值。不过,如果仔细观察,会发现它的 Author 字段只打印了指针地址 - Author:0xc0000900c0 ,没有输出它的内容。

这其实是符合预期的。*Author 是指针类型,它的值自然就是地址。

如果我就想打印 Author 字段的内容,可通过用 fmt.Printf 打印指针实际指向内容。

fmt.Print("%+v\n", article.Author)

输出:

&{Name:poloxue Age:18 Sex:male}

我在测试的时候,发现个有趣的现象:

如果打印的是结构体指针,它会自动解引用,即能把内容打印出来。如上的代码所示,无论是

Printf("%+v\n", article.Author)

还是

Printf("%\n", *article.Author)

都能打印出结构体的内容。

但如果打印的结构体,包含结构体指针字段,则不会将内容打印出来,而只会打印地址,即指针值。

我猜测,如此设计的原因是为了防止深层递归,或者循环引用。

想明白的话,似乎是个显而易见的事情。

实现 String 方法

除了以上将 Author 字段单独拿出打印,我们还有其他方法实现吗?当然有,这就是本节要说的 - 实现 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 继续深度打印。

代码类似于:

if field.Type.Kind() == reflect.Pointer {
    fmt.Println(val.Field(i).Elem())
}

即如果是指针类型,通过 Elem() 继续深入它的内部。

当然,如果你希望得到结构体类型相关信息。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 自定义的优势。不靠套娃,最终得到用了 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:  &amp;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",
}

输出结果为格式化的结构体输出。

这两个库都已经处于很久不更新的状态,但是功能满足我们的需求。

结语

本文主要介绍了 Go 语言中打印结构体的不同方法。我们从简单的 fmt.Printf 到使用反射,甚至是第三方库,我们是有很多选择。

简单主题深入起来,扩展内容也可很丰富。

关于打印结构体这个主题,还有一个部分没有谈到,就是日志如何记录类似结构体等复杂结构类型的变量,毕竟日志对于问题调试至关重要。后面有机会,可单独谈下这个主题。

最后,希望这篇文章能帮助你在打印调试 GO 结构体等复杂结构时,不再迷茫。

以上就是Go打印结构体提升代码调试效率实例详解的详细内容,更多关于Go打印结构体的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文