Go Variable to String

AI 摘要: 本文介绍了Go语言中的fmt.Printf格式变量输出的不同方式:%v, %+v, %#v,并通过例子说明了它们的区别。建议在一般情况下使用fmt.Sprintf("%+v", v)完成日志打印,复杂情况可以考虑使用json.Marshal()方法转换为字符串输出,而对于复杂的数据结构类型,推荐使用goval包实现变量转字符串格式化输出。

1. fmt.Printf格式变量输出

主要注意区分%v ,%+v ,%#v 差异:

  • %v Go默认值打印输出,比如{1 user1}
  • %+v 附带相关属性的打印,比如{id:1 Name:user1}
  • %#v 附带Go语言类型打印,比如fmts.User{id:1, Name:"user1"}

注意: 日志打印,通常用%v%+v就可以,当需要附带属性名称时候使用%+v%#v通常用于查看Go原始的值,比如查看指针的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package fmts

import (
	"encoding/json"
	"fmt"
	"reflect"
	"testing"
)

type User struct {
	id   int
	Name string
}

// 结构体打印
// fmts_test.go:47: struct: {1 user1}, {id:1 Name:user1}, fmts.User{id:1, Name:"user1"}
// fmts_test.go:48: pointer: &{2 user2}, &{id:2 Name:user2}, &fmts.User{id:2, Name:"user2"}
func TestPrintV(t *testing.T) {
	u1 := User{1, "user1"}
	u2 := &User{2, "user2"}

	// %v 不带属性名称 {1 user1}
	// %+v 带属性名称,{id:1 Name:user1}
	// %#v 会带上Go语言结构体名称 fmts.User{id:1, Name:"user1"}
	t.Logf("struct: %v, %+[1]v, %#[1]v", u1)
	t.Logf("pointer: %v, %+[1]v, %#[1]v", u2)
}

2. json.Marshal 序列化成字符串

如果是结构体指针切片,%v 都只会打印切片元素的值而不会打印指针的结果,可以借助json.Marshal() 序列化后打印;

**存在问题:**因为Json序列化仅会针对导出类型做处理,因为Name是导出的,所以支持打印出来,但id非导出,导致无法正常返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// 指针结构体切片打印
// fmts_test.go:66: raw: [0x140001302a0 0x140001302b8], [0x140001302a0 0x140001302b8], []*fmts.User{(*fmts.User)(0x140001302a0), (*fmts.User)(0x140001302b8)}
// fmts_test.go:67: slice: &[0x140001302a0 0x140001302b8], &[0x140001302a0 0x140001302b8], &[]*fmts.User{(*fmts.User)(0x140001302a0), (*fmts.User)(0x140001302b8)}
// fmts_test.go:74: [{"Name":"user1"},{"Name":"user2"}] // Json序列化因为Name是导出的,所以支持打印出来
func TestPrintSlice(t *testing.T) {
	// 如果是结构体指针切片,%v 都只会打印切片元素的值而不会打印指针的结果,
	users := []*User{
		{1, "user1"},
		{2, "user2"},
	}

	// slice - %v和%+v没有什么区别,%#v会打印内部数据结构
	t.Logf("raw: %v, %+[1]v, %#[1]v", users)
	t.Logf("slice: %v, %+[1]v, %#[1]v", &users)

	// json marshal print
	b, err := json.Marshal(users)
	if err != nil {
		t.Error(err)
	}
	t.Logf("%s", b)

	// 通过Reflect打印看看
	t.Logf("print by reflect: %s", GetGoString(users))
}

3. print.Stringer 自定义格式输出

通过String()输出有个好处,就是当结构体为nil时候会正常打印nil而不会直接panic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Member struct {
	id   int
	name string
}

// 让Member结构体实现Strings接口,支持%v打印
func (m *Member) String() string {
	return fmt.Sprintf(`{id:%d,Name:%s}`, m.id, m.name)
}

// 通过String()输出有个好处,就是当结构体为nil时候会正常打印nil而不会直接panic
func TestPrintNilMember(t *testing.T) {
	var m *Member
	// t.Logf("%v", m.id) // will panic
	t.Logf("%v", m)
}

// fmts_test.go:92: raw: [{id:1,Name:MemberA} {id:2,Name:MemberB}], [{id:1,Name:MemberA} {id:2,Name:MemberB}], []*fmts.Member{(*fmts.Member)(0x1400000e2a0), (*fmts.Member)(0x1400000e2b8)}
// 可以看到,
// 	%v、%+v -> 按String()方法打印
//	%#v -> 按Go结构体类型打印,所以内部还是指针
//	结论: 日志打印,通常用%v或%+v就可以,当需要附带属性名称时候,使用%+v;%#v通常用于查看Go原始的值,比如查看指针的值
func TestPrintSlice2(t *testing.T) {
	members := []*Member{
		{1, "MemberA"},
		{2, "MemberB"},
	}
	t.Logf("raw: %v, %+[1]v, %#[1]v", members)
}

当遇到结构体比较复杂,需要打印的时候,发现%+v方法都不是很好,主要原因:

  1. **通用性问题:**实现print.Stringer 接口需要针对每个元素进行罗列打印,比较繁琐
  2. **结构体字段导出问题:**采用json.Marshal 方式会存在内部元素没有导出无法打印的情况,需要调整结构体的字段为导出模式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type Extend struct {
	School string
	Home   string
}

// 结构体指针嵌套
type NestUser struct {
	ID     int
	Name   string
	Favs   []int
	Extend *Extend
	Family map[string]string
}

// fmts_test.go:106: %v => &{100 UserA [4 5] 0x1400014c040 map[father:fa mother:mo]}
// fmts_test.go:107: %+v => &{ID:100 Name:UserA Favs:[4 5] Extend:0x1400014c040 Family:map[father:fa mother:mo]}
// fmts_test.go:108: %#v => &fmts.NestUser{ID:100, Name:"UserA", Favs:[]int{4, 5}, Extend:(*fmts.Extend)(0x1400014c040), Family:map[string]string{"father":"fa", "mother":"mo"}}
// 从这个结果看得出很明显的效果,Go不会迭代打印,通常情况日志用%+v包含结构体属性标签更适合Debug
func TestPrintNestStruct(t *testing.T) {
	u1 := &NestUser{
		ID:   100,
		Name: "UserA",
		Favs: []int{4, 5},
		Extend: &Extend{
			School: "SchoolXiao",
			Home:   "HomeTang",
		},
		Family: map[string]string{
			"mother": "mo",
			"father": "fa",
		},
	}
	t.Logf(`%%v => %v`, u1)
	t.Logf(`%%+v => %+v`, u1)
	t.Logf(`%%#v => %#v`, u1)
}

4. reflect 反射实现通用格式输出

4.1. github.com/hold7techs/goval

易于使用,参考: https://github.com/hold7techs/goval,主要是基于反射+递归,然后借助strings.Builder() 做了一定优化,生产基本可用!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 尝试用反射
// 通过反射获取到v的值表示形式,会不断迭代下去
// fmts_test.go:136: netUser=>&fmts.NestUser{ID: 100, Name: "UserA", Favs: []int{4, 5}, Extend: &fmts.Extend{School: "SchoolXiao", Home: "HomeTang"}, Family: map[string]string{"mother": "mo", "father": "fa"}}
// fmts_test.go:148: netMap=>map[string]*fmts.Extend{"ext2": &fmts.Extend{School: "School2", Home: "Home2"}, "ext1": &fmts.Extend{School: "School1", Home: "Home1"}}
func TestReflectPrint(t *testing.T) {
	u1 := &NestUser{
		ID:   100,
		Name: "UserA",
		Favs: []int{4, 5},
		Extend: &Extend{
			School: "SchoolXiao",
			Home:   "HomeTang",
		},
		Family: map[string]string{
			"mother": "mo",
			"father": "fa",
		},
	}

	// 嵌套打印
	t.Logf("netUser=>%s", goval.ToTypeString(u1))

	m := map[string]*Extend{
		"ext1": {
			School: "School1",
			Home:   "Home1",
		},
		"ext2": {
			School: "School2",
			Home:   "Home2",
		},
	}
	t.Logf("netMap=>%s", goval.ToTypeString(m))
}

4.2. github.com/lupguo/go-render/render

fork过来的包做了go.mod,整体也不错,代码稍复杂一些

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// fmts_test.go:173: netUser=>(*fmts.NestUser){ID:100, Name:"UserA", Favs:[]int{4, 5}, Extend:(*fmts.Extend){School:"SchoolXiao", Home:"HomeTang"}, Family:map[string]string{"father":"fa", "mother":"mo"}}
// fmts_test.go:185: netMap=>map[string]*fmts.Extend{"ext1":(*fmts.Extend){School:"School1", Home:"Home1"}, "ext2":(*fmts.Extend){School:"School2", Home:"Home2"}}
func TestReflectRenderPrint(t *testing.T) {
	u1 := &NestUser{
		ID:   100,
		Name: "UserA",
		Favs: []int{4, 5},
		Extend: &Extend{
			School: "SchoolXiao",
			Home:   "HomeTang",
		},
		Family: map[string]string{
			"mother": "mo",
			"father": "fa",
		},
	}

	// 嵌套打印
	t.Logf("netUser=>%s", render.Render(u1))

	m := map[string]*Extend{
		"ext1": {
			School: "School1",
			Home:   "Home1",
		},
		"ext2": {
			School: "School2",
			Home:   "Home2",
		},
	}
	t.Logf("netMap=>%s", render.Render(m))
}

4.3. Benchmak

$ go test -benchmem -bench ., 可以看到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// $ go test -benchmem -bench .
// goos: darwin
// goarch: arm64
// pkg: x-learn/refs/fmts
// BenchmarkRender-10                585310              2006 ns/op            1032 B/op         25 allocs/op
// BenchmarkGovalToString-10         611055              1972 ns/op             912 B/op         44 allocs/op
// PASS
// ok      x-learn/refs/fmts       3.446s

func BenchmarkRender(b *testing.B) {
	u1 := &NestUser{
		ID:   100,
		Name: "UserA",
		Favs: []int{4, 5},
		Extend: &Extend{
			School: "SchoolXiao",
			Home:   "HomeTang",
		},
		Family: map[string]string{
			"mother": "mo",
			"father": "fa",
		},
	}
	for i := 0; i < b.N; i++ {
		render.Render(u1)
	}
}

func BenchmarkGovalToString(b *testing.B) {
	u1 := &NestUser{
		ID:   100,
		Name: "UserA",
		Favs: []int{4, 5},
		Extend: &Extend{
			School: "SchoolXiao",
			Home:   "HomeTang",
		},
		Family: map[string]string{
			"mother": "mo",
			"father": "fa",
		},
	}
	for i := 0; i < b.N; i++ {
		goval.ToString(u1)
	}
}

小结

通常情况,通过fmt.Sprintf("%+v", v)方式就可以完成日志打印,稍微复杂一些可以考虑通过json.Marshal()方式转字符串输出,但如果牵扯多层嵌套、指针切片、指针Map等复杂机构类型时候,可以考虑使用goval包实现变量转字符串格化输出!