Go-知识-fmt
Go-知识-fmt
- 介绍
- 数值类型
- 字符类型
- 布尔类型
- 其他
- API
- Fprint
- Sprint
- Fprintf
- 格式穷举
- Printf
- Sprintf
- Fprintln
- Println
- Sprintln
- Appendln
- error
- 自定义
Go-知识-fmt
介绍
fmt 实现了格式化输出,并提供了相应的占位符。
支持的数据类型如下:
- 数值类型:整数类型,浮点类型
- 字符类型
- 指针类型
- 布尔类型
- 其他
数值类型
- %b : 二进制
- %o : 八进制
- %x : 十六进制
- %X : 十六进制
- %d : 十进制
- %f : 浮点类型
- %e : 科学计数法
- %E : 科学计数法
试一试
func TestFmt(t *testing.T) {
number := 100.234
numberInt := 45
fmt.Printf("整数%%d \t %d\n", numberInt)
fmt.Printf("八进制%%o \t %o\n", numberInt)
fmt.Printf("十六进制%%x \t %x\n", numberInt)
fmt.Printf("十六进制%%X \t %X\n", numberInt)
fmt.Printf("布尔值%%b \t %b\n", numberInt)
fmt.Printf("浮点值%%f \t %f\n", number)
fmt.Printf("科学计数法%%e \t %e\n", number)
fmt.Printf("科学计数法%%E \t %E\n", number)
}
执行结果如下
字符类型
- %s : 字符类型
- %q : 带双引号
如下代码
func TestFmtString(t *testing.T) {
str := "hello world"
fmt.Printf("字符类型%%s \t %s\n", str)
fmt.Printf("待双引号%%q \t %q\n", str)
}
执行结果如下
布尔类型
- %t : 布尔类型
func TestFmtBool(t *testing.T) {
b := true
fmt.Printf("布尔类型%%b \t %t\n", b)
}
其他
- %T : 判断类型(输出类型)
- %p : 指针类型
- %v : 默认格式
- %#v : 带语法的格式
func TestFmtOther(t *testing.T) {
a := 1
b := 2.0
ok := true
ptr := &a
s := struct {
Name string
}{
Name: "test",
}
fmt.Printf("类型%%T \t %T\n", a)
fmt.Printf("类型%%T \t %T\n", b)
fmt.Printf("类型%%T \t %T\n", ok)
fmt.Printf("类型%%T \t %T\n", ptr)
fmt.Printf("类型%%T \t %T\n", s)
fmt.Println()
fmt.Printf("指针%%p \t %p\n", ptr)
fmt.Printf("指针%%p \t %p\n", &a)
fmt.Printf("默认格式%%v \t %v\n", s)
fmt.Printf("带语法格式%%#v \t %#v\n", s)
}
API
Fprint/Fprintf/Fprintln
: 带格式的输出Print/Printf/Println
: 标准输出Sprint/Sprintf/Sprintln
: 格式化内容为 string
Fprint/Print/Sprint
表示使用默认的格式输出或者格式化内容,Fprintf/Printf/Sprintf
表示使用指定的格式输出或格式化内容,Fprintln/Println/Sprintln
表示使用默认的格式输出或格式化内容,同时会在最后加上换行符\n
Fprint
源码如下:
//
func Fprint(w io.Writer, a ...any) (n int, err error) {
p := newPrinter()
p.doPrint(a)
n, err = w.Write(p.buf)
p.free()
return
}
newPrinter
干了啥
func newPrinter() *pp {
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
ppFree
又是个啥
var ppFree = sync.Pool{
New: func() any { return new(pp) },
}
ppFree 是一个缓存池
提高对象的利用率
p := ppFree.Get().(*pp)
是从缓存池中拿一个 pp 结构,因为缓存池是 any 类型的,所以需要进行类型强转
Pp 结构如下:
type pp struct {
buf buffer
// arg holds the current item, as an interface{}.
arg any
// value is used instead of arg for reflect values.
value reflect.Value
// fmt is used to format basic items such as integers or strings.
fmt fmt
// reordered records whether the format string used argument reordering.
reordered bool
// goodArgNum records whether the most recent reordering directive was valid.
goodArgNum bool
// panicking is set by catchPanic to avoid infinite panic, recover, panic, ... recursion.
panicking bool
// erroring is set when printing an error string to guard against calling handleMethods.
erroring bool
// wrapErrs is set when the format string may contain a %w verb.
wrapErrs bool
// wrappedErrs records the targets of the %w verb.
wrappedErrs []int
}
有很多的属性
p.fmt.init(&p.buf)
设置pp结构的 fmt 的属性
其中的 fmt 结构
type fmt struct {
buf *buffer
fmtFlags
wid int // width
prec int // precision
// intbuf is large enough to store %b of an int64 with a sign and
// avoids padding at the end of the struct on 32 bit architectures.
intbuf [68]byte
}
type fmtFlags struct {
widPresent bool
precPresent bool
minus bool
plus bool
sharp bool
space bool
zero bool
// For the formats %+v %#v, we set the plusV/sharpV flags
// and clear the plus/sharp flags since %+v and %#v are in effect
// different, flagless formats set at the top level.
plusV bool
sharpV bool
}
对于 pp 结构中 fmt 的初始化
func (f *fmt) clearflags() {
f.fmtFlags = fmtFlags{}
}
func (f *fmt) init(buf *buffer) {
f.buf = buf
f.clearflags()
}
设置了缓存区,同时清空了标志位。因为 pp 的指针值是缓存的,拿出来的可能是之前用过的,所以需要先初始化清空一下才能使用。
p := newPrinter()
之后就是 p.doPrint(a)
, 看下 doPrint
func (p *pp) doPrint(a []any) {
prevString := false
for argNum, arg := range a {
isString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String
// Add a space between two non-string arguments.
if argNum > 0 && !isString && !prevString {
p.buf.writeByte(' ')
}
p.printArg(arg, 'v')
prevString = isString
}
}
先判断是不是字符串,如果不是字符串,那么在每个值中间加一个 空格
接着调用 printArg
打印
printArg
就是针对go支持的所有格式化占位符,进行替换的一个过程
等待占位符替换完成后,将缓存区内的数据,写入到传入的writer
里,n, err = w.Write(p.buf)
最后一步的 p.free()
是把从缓存池中拿出来的对象还回去,并且刷新和释放缓存区
func (p *pp) free() {
// Proper usage of a sync.Pool requires each entry to have approximately
// the same memory cost. To obtain this property when the stored type
// contains a variably-sized buffer, we add a hard limit on the maximum
// buffer to place back in the pool. If the buffer is larger than the
// limit, we drop the buffer and recycle just the printer.
//
// See https://golang.org/issue/23199.
if cap(p.buf) > 64*1024 {
p.buf = nil
} else {
p.buf = p.buf[:0]
}
if cap(p.wrappedErrs) > 8 {
p.wrappedErrs = nil
}
p.arg = nil
p.value = reflect.Value{}
p.wrappedErrs = p.wrappedErrs[:0]
ppFree.Put(p)
}
Print
就是调用了 Fprint
,只是传入的writer
是标准输出设备
func Print(a ...any) (n int, err error) {
return Fprint(os.Stdout, a...)
}
Sprint
Sprint
和 Fprint
差不多,Sprint
不需要将缓存区里面的数据写入到 writer
了,直接转为字符串返回即可
func Sprint(a ...any) string {
p := newPrinter()
p.doPrint(a)
s := string(p.buf)
p.free()
return s
}
Fprintf
Fprintf
相比 Fprint
多了一个入参,也就是格式串。格式串就是带有占位符的字符串
func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()
return
}
其主要逻辑与 Fprint
相同,区别在于 doPrintf
通过 for i := 0; i < end; 循环逐字符解析格式字符串,分为两个处理阶段:
- 普通字符:直接写入缓冲区
- 格式化指令:以 % 开头的部分
遇到 % 后执行以下步骤:
- 解析标志(#, 0, +, -, )
- 处理参数索引(如 %[3]d)
- 解析宽度和精度(支持 * 动态值)
- 处理特殊动词:
case ‘w’:
p.wrappedErrs = append(p.wrappedErrs, argNum)
case ‘v’:
// 处理 Go 语法格式
参数处理
- 使用 argNum 跟踪当前参数索引
- 调用 printArg 进行实际格式化操作
- 支持参数重排序(%[n] 语法)
- 错误包装:处理 %w 时记录错误参数位置
- 类型反射:通过 reflect.Value 处理不同类型
- 性能优化:使用 buffer 结构进行高效字符串拼接
- 语法兼容:支持完整的 printf 语法规范
格式穷举
格式串 | 输出示例 | 说明 |
---|---|---|
%v | 42 | 通用格式,自动匹配类型 |
%+v | {Name: ""} | 带字段名的结构体输出 |
%#v | "go" | Go语法表示值(带类型信息) |
%T | float64 | 输出值的类型 |
%d | 255 | 十进制整数 |
%b | 101 | 二进制表示 |
%o | 10 | 八进制表示 |
%x | f | 小写十六进制 |
%X | F | 大写十六进制 |
%c | A | Unicode字符 |
%f | 3.141500 | 默认精度浮点数 |
%.2f | 3.14 | 保留2位小数 |
%e | 1.234500e+03 | 科学计数法表示 |
%g | 1.23456789e+08 | 自动选择最紧凑表示法 |
%s | hello | 原始字符串输出 |
%q | "go" | 带双引号的字符串 |
%x | 676f | 字符串的十六进制编码 |
%5d | 42 | 右对齐宽度5 |
%-5d | 42 | 左对齐宽度5 |
%05d | 00042 | 零填充宽度5 |
%+d | +42 | 显示正负号 |
% d | 42 | 正数前留空格 |
%[2]d %[1]d | 2 1 | 参数索引重排序 |
%*d | 42 | 动态宽度(参数指定宽度5) |
%.*f | 3.14 | 动态精度(参数指定精度2) |
%p | 0xc0000160a8 | 指针地址 |
%w | error | 错误包装(需配合errors包使用) |
%v | [1 2] | 切片/数组的默认输出 |
%#v | []int{1, 2} | 切片/数组的Go语法表示 |
%+#10.3f | +3.142 | 组合格式:符号+宽度10+精度3 |
%[3]d %[1]s | 42 hello | 多参数混合索引 |
%+v | {X:1 Y:2} | 结构体带字段名输出 |
%U | U+0041 | Unicode码点格式 |
%#b | 0b101 | Go语法二进制表示 |
%#o | 0o10 | Go语法八进制表示 |
%#x | 0xf | Go语法十六进制表示 |
%s | MyInt | 自定义类型实现String()方法时的输出 |
%d | %!d(string=text) | 类型不匹配时的错误提示 |
%d | %!d(MISSING) | 缺少参数时的错误提示 |
Printf
Printf
很简单直接用标准输出调用 Fprintf
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
Sprintf
Sprintf
不需要写入write
,直接将缓存区的内容返回即可
func Sprintf(format string, a ...any) string {
p := newPrinter()
p.doPrintf(format, a)
s := string(p.buf)
p.free()
return s
}
Fprintln
Fprintln
基本上也大差不差的,核心是 doPrintln
func Fprintln(w io.Writer, a ...any) (n int, err error) {
p := newPrinter()
p.doPrintln(a)
n, err = w.Write(p.buf)
p.free()
return
}
doPrintln
在 doPrint
的基础上,在最后加了换行符\n
,都是使用默认格式打印的
func (p *pp) doPrintln(a []any) {
for argNum, arg := range a {
if argNum > 0 {
p.buf.writeByte(' ')
}
p.printArg(arg, 'v')
}
p.buf.writeByte('\n')
}
Println
Println
将标准输出作为 writer
请求 Fprintln
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
Sprintln
Sprintln
不需要写入writer
直接将缓冲区的内容返回即可
func Sprintln(a ...any) string {
p := newPrinter()
p.doPrintln(a)
s := string(p.buf)
p.free()
return s
}
Appendln
追加换行
func TestAppln(t *testing.T) {
var s []byte
s = fmt.Appendln(s, "hello world")
s = fmt.Appendln(s, "hello world")
s = fmt.Appendln(s, "hello world")
fmt.Print(string(s))
}
error
在格式串中 %w
是错误类型的格式串
除此之外,fmt有针对错误的函数Errorf
在 doPrintf
里面会统计 %w
的信息
如果只有一个 %w
,那么直接使用 errors.New
,如果有多个,需要进行warp处理
errors.New
实际上就是 stringError
warpErrors
就是多个错误
自定义
在java等一些语言中,输出调用自动从 Object
继承的ToString
方法,将对象信息转为字符串。
在go里面也有类似的接口
Stringer
接口定义的String
接口就是默认的结构体转字符串的调用方法
type T struct {
Name string
}
func (t T) String() string {
return fmt.Sprintln(fmt.Sprintf("%q", t.Name))
}
func TestString(t *testing.T) {
name := T{"hello world"}
fmt.Print(name)
}
除此之外,还有一个 接口GoStringer
定义的 GoString
用于适配 %#v
才会调用
type T struct {
Name string
}
func (t T) String() string {
return fmt.Sprintln(fmt.Sprintf("%q", t.Name))
}
func (t T) GoString() string {
return fmt.Sprintln(fmt.Sprintf("struct T : %q", t.Name))
}
func TestString(t *testing.T) {
name := T{"hello world"}
fmt.Print(name)
fmt.Printf("%#v", name)
}