浅谈Golang格式化打印

前言

不管是为了调试,还是为了啥。我们在工作中经常会需要对结构体或一些值啥的进行打印,一个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的性能。由于只是浅谈,并没有详细展开各个点,其实都有很多细节可以讲,可以以后各个分别讲一章。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值