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
方法都不是很好,主要原因:
- **通用性问题:**实现
print.Stringer
接口需要针对每个元素进行罗列打印,比较繁琐 - **结构体字段导出问题:**采用
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
包实现变量转字符串格化输出!