目录

Go 实现控制台完美打印结构体数据

控制台打印结构数据的问题:用默认的 fmt.Print 打印结构体,要么只显示类型不显示字段,要么嵌套结构全是乱码,调试时根本看不清数据全貌。

今天分享一下如何实现完美打印数据的方法,希望对大家有所帮助

一、基础:fmt 包

如果只是打印简单结构体,不需要嵌套美化,fmt 包的格式化参数完全能满足需求,这也是日常调试用得最多的基础方法。

核心是掌握三个关键参数:%v、%+v、%#v。

func main() {
    // 构造测试数据
    age := 28
    detail := "科技园区8号楼"
    user := User{
        Name:   "张三",
        Age:    &age,
        Gender: "男",
        Address: Address{
            Province: "广东",
            City:     "深圳",
            Detail:   &detail,
        },
        CreateAt: time.Now(),
        Hobby:    []string{"篮球", "编程"},
        Score:    map[string]int{"数学": 90, "英语": 85},
    }

    // 1. %v:基础打印,不显示字段名,指针显示地址
    fmt.Println("=== %v 打印 ===")
    fmt.Printf("%v\n", user)

    // 2. %+v:显示字段名,适合查看字段对应的值
    fmt.Println("\n=== %+v 打印 ===")
    fmt.Printf("%+v\n", user)

    // 3. %#v:显示结构体完整类型和字段名,适合定位类型问题
    fmt.Println("\n=== %#v 打印 ===")
    fmt.Printf("%#v\n", user)
}

输出效果分析:%+v 是基础用法里的“性价比之王”,能清晰显示字段名和对应值,但嵌套结构的指针还是会显示地址;

比如Age字段会显示(*int)(0xc00001a0a0),不适合复杂场景。

二、标准:json 包

当结构体有嵌套、指针或切片字段时,json 包的 MarshalIndent 方法是更优选择,能自动格式化嵌套结构,还能处理指针字段的实际值。缺点是会忽略未导出字段(首字母小写的字段),但调试业务代码时基本不影响。

import "encoding/json"

func main() {
    // 构造测试数据(同上面示例)
    age := 28
    detail := "科技园区8号楼"
    user := User{/* 赋值省略 */}

    fmt.Println("=== json 格式化打印 ===")
    // MarshalIndent参数:数据、前缀、缩进符、缩进宽度
    data, err := json.MarshalIndent(user, "", "  ")
    if err != nil {
        fmt.Printf("json序列化失败:%v\n", err)
        return
    }
    fmt.Println(string(data))
}

输出效果分析:会以 JSON 格式缩进显示,指针字段会自动解析为实际值,嵌套的 Address 结构体也会完整展开,甚至会尊重结构体的json tag(比如 Gender 字段会显示为"gender")。

但 time.Time 类型会序列化为 ISO 格式时间字符串,对部分开发者来说可能不够直观。

三、进阶:pprint 库

如果需要更美观的打印效果,比如区分不同数据类型(切片、map 标颜色)、控制缩进长度,第三方库 pprint 是最佳选择。

我常用的是 github.com/davecgh/go-spew,功能强大且配置灵活。

# 执行安装
go get github.com/davecgh/go-spew/spew

实践代码

import "github.com/davecgh/go-spew/spew"

func main() {
    // 构造测试数据(同上面示例)
    age := 28
    detail := "科技园区8号楼"
    user := User{/* 赋值省略 */}

    fmt.Println("=== pprint 美化打印 ===")
    // 基础用法:直接打印
    spew.Dump(user)

    // 高级用法:自定义配置(缩进、是否显示类型等)
    fmt.Println("\n=== 自定义配置打印 ===")
    config := spew.ConfigState{
        Indent:                  "  ",  // 缩进2个空格
        DisablePointerAddresses: true,  // 不显示指针地址
        DisableCapacities:       true,  // 不显示切片容量
        ShowInlinePointers:      true,  // 显示指针指向的实际值
    }
    config.Dump(user)
}

输出效果分析

默认会用不同颜色区分数据类型(终端支持的话),切片和 map 会显示长度,指针会同时显示地址和实际值;

通过配置还能隐藏不需要的信息,比如调试时不需要看指针地址就可以关闭,非常灵活。

四、自定义

实现 String() 方法。

如果业务有特殊打印需求,比如只显示关键字段、格式化时间字段,最灵活的方式是给结构体实现 String() 方法,完全自定义打印格式。这是在打印业务实体时最常用的技巧。

// 给User结构体实现String()方法
func (u User) String() string {
    // 处理指针为nil的情况,避免空指针panic
    ageStr := "未知"
    if u.Age != nil {
        ageStr = strconv.Itoa(*u.Age)
    }

    detailStr := "未填写"
    if u.Address.Detail != nil {
        detailStr = *u.Address.Detail
    }

    // 格式化时间为本地时间字符串
    createAtStr := u.CreateAt.Format("2006-01-02 15:04:05")

    // 拼接切片字段
    hobbyStr := strings.Join(u.Hobby, ", ")

    // 拼接map字段
    scoreStr := ""
    for subject, score := range u.Score {
        scoreStr += fmt.Sprintf("%s:%d ", subject, score)
    }

    return fmt.Sprintf(`用户信息:
  姓名: %s
  年龄: %s
  性别: %s
  地址: %s-%s-%s
  创建时间: %s
  爱好: %s
  成绩: %s`,
        u.Name, ageStr, u.Gender,
        u.Address.Province, u.Address.City, detailStr,
        createAtStr, hobbyStr, scoreStr)
}

func main() {
    // 构造测试数据(同上面示例)
    age := 28
    detail := "科技园区8号楼"
    user := User{/* 赋值省略 */}

    // 直接打印结构体,会自动调用String()方法
    fmt.Println("=== 自定义String()方法打印 ===")
    fmt.Println(user)
}

优势分析:完全按照业务需求定制,比如把时间格式化为“2006-01-02 15:04:05”这种直观格式,把切片用逗号拼接,关键是能避免空指针panic(比如判断 Age 为 nil 时显示“未知”)。

常见问题

Q1. 嵌套结构体打印不全,只显示类型名

现象:打印 User 结构体时,Address 字段显示为main.Address{},看不到具体的省市区信息。

原因:使用了 %v 或未指定格式化参数,默认打印只会显示顶层字段的基础信息。

解决方案:改用 %+v 打印,或用 json.MarshalIndent 序列化,两种方式都能完整展开嵌套结构。

如果需要自定义格式,就在嵌套的 Address 结构体也实现 String() 方法。

Q2. 指针字段打印显示内存地址,看不到实际值

现象:Age 字段打印为(*int)(0xc00001a0a0),不知道具体年龄是多少。

解决方案:三种思路可选:

① 用 json.MarshalIndent 自动解析指针值;

② 用 spew 库并开启 ShowInlinePointers 配置;

③ 自定义 String() 方法时手动解引用(注意判断 nil)。

推荐前两种,更简洁。

Q3. 未导出字段(首字母小写)打印不显示

现象:结构体里有个phone string字段(首字母小写),用任何方式打印都看不到该字段。

原因:Go 的导出规则限制,未导出字段只能在当前包访问,json 包和第三方库都无法读取,自然无法打印。

解决方案

① 调试期间临时把字段改为导出(首字母大写),调试完成后再改回去;

② 在当前包内自定义 String() 方法,通过结构体方法访问未导出字段并拼接(推荐,更安全)。

Q4. time.Time字段打印格式不直观

现象:用 %+v 打印时,CreateAt 字段显示为2024-05-20 14:30:00 +0800 CST m=+0.001000001,冗余信息太多。

解决方案

① 用自定义 String() 方法格式化时间;

② 给 time.Time 字段加 json tag 指定格式(仅 json 序列化时生效):

总结

没必要追求“万能方案”,根据调试场景选择最合适的即可:

  • 简单结构体调试:直接用 fmt.Printf("%+v", struct),最快最省事;
  • 嵌套/指针结构调试:优先用 json.MarshalIndent,无依赖且格式清晰;
  • 复杂结构或开源项目调试:用 spew 库,支持颜色和灵活配置,美观易读;
  • 业务实体定制打印:实现 String() 方法,按业务需求展示关键信息,避免冗余。

其实控制台打印结构数据的核心就是“按需格式化”,不用追求花哨的技巧,能清晰看到调试所需的信息就是“完美打印”。

如果大家有其他好用的打印技巧,欢迎在评论区交流~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-fmt-print-field-name/

备用原文链接: https://blog.fiveyoboy.com/articles/go-fmt-print-field-name/