开发者

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

开发者 https://www.devze.com 2024-08-14 13:18 出处:网络 作者: 波罗学
目录引言定义结构体使用 fmt.Printf实现 String 方法json.MarshalIndentreflect 包打印复杂结构性能压测三方库go-spewpretty结语引言
目录
  • 引言
  • 定义结构体
  • 使用 fmt.Printf
  • 实现 String 方法
  • json.MarshalIndent
  • reflect 包打印复杂结构
  • 性能压测
  • 三方库
    • go-spew
    • pretty
  • 结语

    引言

    不知道大家是否遇到打印复杂结构的需求?

    结构体的特点是有点像是一个盒子,但不同于 slice 与 map,它里面可以放很多不同类型的东西,如数字、字符串、slice、map 或者其他结构体。

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

    但,如果我们想看看盒子中内容,该怎么办呢?这时我们就要能打印结构体了。

    打印结构体的能力其实挺重要的,它能帮我们检查理解代码,提高调试效率,确保代码运行正确。

    本文让我们以此为话题,聊聊 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 结构体指针,用于保存作者信息。

    我将先介绍四种基本的方式。

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

    使用 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)

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

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

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

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

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

    实现 String 方法

    除了以上将 Author 字段单独拿出打印,我们还有其他方法实现吗?当然有,这就是本节要说的 - 实现 String 方法。

    这其实是 Go 提供的一种机制,一个类型如果满足 Stringer 接口,即实现了 String 方法,打印时返回的就是 String 方法的返回内容。

    Stringer 定义如下:

    type StrraMWGoVinger 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)
    }
    
    fjsunc 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 方法中定义的形式。现在,我们可以随心所欲定义打印格式了。

    这种方式还有一个特点,就是性能高。毕竟,它没有任何啰嗦,直接到拿到结果。

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

    到这里,我们已经有能力打印结构体了。但这里也有些缺点。

    首先,不够美观,输出结构易读性差。这不利于快速定位。

    其次,每次都要自定义输出。如果只是为了 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

    Typephp: 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。也算是类型内存空间的占用。

    性能压测

    我尝试了不同的打印方法后,也进行了一个简单的性能测试。

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

    测试结果如下所示:

    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 Strucandroidture 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打印结构体的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号