前言
不管是为了调试,还是为了啥。我们在工作中经常会需要对结构体或一些值啥的进行打印,一个pretty的打印格式能够给我们带来良好的心情,关键是可以提高我们调试的效率,一下就看到哪里出了问题。那么我们可以咋么来打印呢?
我们先随便搞个结构体吧:
package demo
import (
"time"
)
type Gender int
const (
Male Gender = iota
Female
)
type User struct {
Name string
Age int
Gender
Address string
BirthDate time.Time
Introduction string
Fellow *User
}
几种打印方式
printf
最基础的一种方式就是使用fmt的printf系列函数来打印,我们来看看几个最常用的方式打印出来的效果:
example := &User{
Name: "string",
Age: 26,
Gender: Male,
Address: "this is my home street",
BirthDate: time.Now().UTC(),
Introduction: "I'm a happy boy. I like playing guitar.",
Fellow: &User{
Name: "son",
Age: 1,
Gender: Male,
Address: "same with string",
BirthDate: time.Now().Add(-5 * time.Second),
Introduction: "son of string",
}}
fmt.Printf("%v\n%+v\n%#+v\n", example, example, example)
输出:
&{string 26 0 this is my home street 2021-09-23 16:43:07.844691 +0000 UTC I'm a happy boy. I like playing guitar. 0xc0000a2240}
&{Name:string Age:26 Gender:0 Address:this is my home street BirthDate:2021-09-23 16:43:07.844691 +0000 UTC Introduction:I'm a happy boy. I like playing guitar. Fellow:0xc0000a2240}
&demo.User{Name:"string", Age:26, Gender:0, Address:"this is my home street", BirthDate:time.Time{wall:0x3258f638, ext:63768012187, loc:(*time.Location)(nil)}, Introduction:"I'm a happy boy. I like playing guitar.", Fellow:(*demo.User)(0xc0000a2240)}
可以看到
- %v以很简略的方式打出了所有字段,字段名没有,类型名没有,字段之间也没有分割,实在是简约过头了。
- +v把字段名给打出来了,但是也不会继续展开其中的指针,并且也没有类型名。
- #+v倒是即有UserName,又有类型名了,但它还是没有展开指针。然后不该展开的,比如BirthDate的time.Time类型就瞎展开,明明原来那个格式好。
要是是指针数组就更扯淡了:
var example = []*User{{
Name: "string",
Age: 26,
Gender: Male,
Address: "this is my home street",
BirthDate: time.Now().UTC(),
Introduction: "I'm a happy boy. I like playing guitar.",
Fellow: &User{
Name: "son",
Age: 1,
Gender: Male,
Address: "same with string",
BirthDate: time.Now().Add(-5 * time.Second),
Introduction: "son of string",
},
}, {
Name: "lemon",
Age: 27,
Gender: Male,
Address: "this is my girl",
BirthDate: time.Now().UTC(),
Introduction: "I'm a happy girl. I gogogo me.",
},
}
func TestUser_printf(t *testing.T) {
fmt.Printf("%v\n%+v\n%#+v\n", example, example, example)
}
输出:
[0x1230b00 0x1230bc0]
[0x1230b00 0x1230bc0]
[]*demo.User{(*demo.User)(0x1230b00), (*demo.User)(0x1230bc0)}
这简直啥都没说嘛。。
于是我们可以简单粗暴的解决这个问题。
Render
我们可以通过专门的打印函数实现来完成格式化打印,比如"github.com/luci/go-render/render"
package demo
import (
"fmt"
"github.com/luci/go-render/render"
"testing"
"time"
)
var example = []*User{{
Name: "string",
Age: 26,
Gender: Male,
Address: "this is my home street",
BirthDate: time.Now().UTC(),
Introduction: "I'm a happy boy. I like playing guitar.",
Fellow: &User{
Name: "son",
Age: 1,
Gender: Male,
Address: "same with string",
BirthDate: time.Now().Add(-5 * time.Second),
Introduction: "son of string",
},
}, {
Name: "lemon",
Age: 27,
Gender: Male,
Address: "this is my girl",
BirthDate: time.Now().UTC(),
Introduction: "I'm a happy girl. I gogogo me.",
},
}
func TestUser_printf(t *testing.T) {
fmt.Printf("%v\n", render.Render(example))
}
输出:
[]*demo.User{(*demo.User){Name:"string", Age:26, Gender:demo.Gender(0), Address:"this is my home street", BirthDate:time.Time{wall:176993000, ext:63768013141, loc:(*time.Location)(nil)}, Introduction:"I'm a happy boy. I like playing guitar.", Fellow:(*demo.User){Name:"son", Age:1, Gender:demo.Gender(0), Address:"same with string", BirthDate:time.Time{wall:13856250540789117672, ext:-4999739925, loc:(*time.Location){name:"", zone:[]time.zone(nil), tx:[]time.zoneTrans(nil), extend:"", cacheStart:0, cacheEnd:0, cacheZone:(*time.zone)(nil)}}, Introduction:"son of string", Fellow:(*demo.User)(nil)}}, (*demo.User){Name:"lemon", Age:27, Gender:demo.Gender(0), Address:"this is my girl", BirthDate:time.Time{wall:176995000, ext:63768013141, loc:(*time.Location)(nil)}, Introduction:"I'm a happy girl. I gogogo me.", Fellow:(*demo.User)(nil)}}
打的十分的全面,所有细节都打出来了,数组也没问题。
但还是那回事,太细节了,把time.Time内部结构中一堆根本不关心的东西都打出来了;另外,那个枚举类型打成了Gender(0),一般我们希望打印成对应的枚举值,也就是Male或Female。
实际中,在快速的开发中,我经常会使用这个函数来快速地打印结构体,方便调试。事实上,只要结构体内没有time.Time,我也很乐意使用render来打印结构体。
但是这个函数的问题也很多,最大的问题是他内部是使用反射来实现的,性能堪忧。层级越复杂,性能越差。后面我们会简单地对比下性能。
Stringer接口
更优雅地,我会顺手给我写的结构体或者类实现fmt.Stringer接口。
// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
String() string
}
原理是:fmt.printf等格式化打印函数会默认先确定类型有没有实现Stringer接口,如果实现了的话就会直接调用String()来获取打印结果,而不是走默认格式化方法。
这样,我就可以轻松的定制我想要的输出。比如我大概会为这个User结构体这样实现Stringer接口:
……
func (u *User) String() string {
return fmt.Sprintf("User{Name:%q, Age:%v, Gender:%v, Address:%q, BrithDate:%v, Introduction:%q, Fellow:%v}",
u.Name, u.Age, u.Gender, u.Address, u.BirthDate, u.Introduction, u.Fellow)
}
这样,%v或%+v打印出来的效果就是
var example = []*User{{
Name: "string",
Age: 26,
Gender: Male,
Address: "this is my home street",
BirthDate: time.Now().UTC(),
Introduction: "I'm a happy boy. I like playing guitar.",
Fellow: &User{
Name: "son",
Age: 1,
Gender: Male,
Address: "same with string",
BirthDate: time.Now().Add(-5 * time.Second),
Introduction: "son of string",
},
}, {
Name: "lemon",
Age: 27,
Gender: Male,
Address: "this is my girl",
BirthDate: time.Now().UTC(),
Introduction: "I'm a happy girl. I gogogo me.",
},
}
func TestUser_printf(t *testing.T) {
fmt.Printf("%v\n", example)
}
输出:
[User{Name:"string", Age:26, Gender:0, Address:"this is my home street", BrithDate:2021-09-23 17:20:40.197918 +0000 UTC, Introduction:"I'm a happy boy. I like playing guitar.", Fellow:User{Name:"son", Age:1, Gender:0, Address:"same with string", BrithDate:2021-09-24 01:20:35.197918 +0800 CST m=-4.999764012, Introduction:"son of string", Fellow:<nil>}} User{Name:"lemon", Age:27, Gender:0, Address:"this is my girl", BrithDate:2021-09-23 17:20:40.197921 +0000 UTC, Introduction:"I'm a happy girl. I gogogo me.", Fellow:<nil>}]
look,我们通过Stringer接口轻松控制了格式,指针也能正常展开进行打印,时间也不会被展开,而是按照默认的格式,我可以把Gender也定义Stringer接口:
type Gender int
func (g Gender) String() string {
switch g {
case Male:
return "Male"
case Female:
return "Female"
default:
return "<UNSET>"
}
}
const (
Male Gender = iota
Female
)
这样,刚刚的打印出来就成了:
[User{Name:"string", Age:26, Gender:Male, Address:"this is my home street", BrithDate:2021-09-23 17:24:58.271852 +0000 UTC, Introduction:"I'm a happy boy. I like playing guitar.", Fellow:User{Name:"son", Age:1, Gender:Male, Address:"same with string", BrithDate:2021-09-24 01:24:53.271852 +0800 CST m=-4.999768948, Introduction:"son of string", Fellow:<nil>}} User{Name:"lemon", Age:27, Gender:Male, Address:"this is my girl", BrithDate:2021-09-23 17:24:58.271854 +0000 UTC, Introduction:"I'm a happy girl. I gogogo me.", Fellow:<nil>}]
看,Gender那打印的变成了Male,Female了。简直不要太好用。
这个方法还有个好处,就是定制性太高,你可以不只是打印,可以做些额外的事情,输出额外信息,或者隐藏些信息,比如可以隐藏地址,只输出地址的长度:
func (u *User) String() string {
return fmt.Sprintf("User{Name:%q, Age:%v, Gender:%v, Address:<hide>len(%v), BrithDate:%v, Introduction:%q, Fellow:%v}", u.Name, u.Age, u.Gender, len(u.Address), u.BirthDate, u.Introduction, u.Fellow)
}
[User{Name:"string", Age:26, Gender:Male, Address:<hide>len(22), BrithDate:2021-09-23 17:37:50.698815 +0000 UTC, Introduction:"I'm a happy boy. I like playing guitar.", Fellow:User{Name:"son", Age:1, Gender:Male, Address:<hide>len(16), BrithDate:2021-09-24 01:37:45.698815 +0800 CST m=-4.999650007, Introduction:"son of string", Fellow:<nil>}} User{Name:"lemon", Age:27, Gender:Male, Address:<hide>len(15), BrithDate:2021-09-23 17:37:50.698818 +0000 UTC, Introduction:"I'm a happy girl. I gogogo me.", Fellow:<nil>}]
有没很好用?hhh
为枚举类型自动生成Stringer接口实现
还有点不爽,还得为枚举类型一个个case写对应的字符串,其实Go给我们提供了自动生成的工具。
我们这样改下:
//go:generate stringer -type=Gender
type Gender int
const (
Male Gender = iota
Female
)
然后在同个目录下:
$ go generate
自动就冒出了个gender_string.go
// Code generated by "stringer -type=Gender"; DO NOT EDIT.
package demo
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Male-0]
_ = x[Female-1]
}
const _Gender_name = "MaleFemale"
var _Gender_index = [...]uint8{0, 4, 10}
func (i Gender) String() string {
if i < 0 || i >= Gender(len(_Gender_index)-1) {
return "Gender(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Gender_name[_Gender_index[i]:_Gender_index[i+1]]
}
这样就自动实现了枚举类型的Stringer,我们来测试下:
func TestGender_String(t *testing.T) {
for i := 0; i < 5; i++ {
fmt.Println(Gender(i))
}
}
输出:
$ go test -v -run=TestGender_String
=== RUN TestGender_String
Male
Female
Gender(2)
Gender(3)
Gender(4)
--- PASS: TestGender_String (0.00s)
PASS
ok ……/demo 1.605s
效果比自己写的好对吧。而且自动生成的这个文件也有很多值得学习研究的地方。
性能比对
我们看看render和stringer的性能对比,就用上面这个简单的结构体加示例来吧。
func Benchmark_default1(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%v", example)
}
}
func Benchmark_default2(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%v", render.Render(example))
}
}
其中一个输出:
$ go test -v -bench=. -run=none
goos: darwin
goarch: amd64
pkg: ……/demo
Benchmark_default1
Benchmark_default1-12 68085 16994 ns/op
Benchmark_default2
Benchmark_default2-12 31875 36993 ns/op
PASS
ok ……/demo 3.663s
我们可以看到,就是这么简单一个结构体,render的性能能比直接自己实现的简单打印的性能差了一倍以上。因此虽然render很方便,在性能要求高的接口上要慎用。
结语
本文简单讲了下常用的几种格式化打印方法。包括自带的printf,render函数,用stringer定制打印的内容,以及通过go generate工具自动为枚举类型生成stringer实现。并简单测量了下render的性能。由于只是浅谈,并没有详细展开各个点,其实都有很多细节可以讲,可以以后各个分别讲一章。