从源码学习 Go 标准库(一):fmt - format(1)

369 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

前言

本系列文章将以源码和文档为依据,梳理标准库的内容,拓展对标准库的认识,并进一步探索标准库的使用方法。

第一章的主角是 fmt 包,它包括 format print scan errors 这四个部分,我们将按照这个顺序来依次分析。

本篇文章将带大家阅览 format.go,并分析 fmt 的原始格式化程序是如何通过不同的格式化符来控制内容的打印格式。

备注:本系列文章使用的是 go 1.19 源码:

github.com/golang/go/t…

format && formatter

常量与类型

先来看看 format.go 中都定义了哪些常量和类型。

常量:
ldigits = "0123456789abcdefx"
udigits = "0123456789ABCDEFX"
signed  = true
unsigned = false

类型:
fmtFlags{ widPresent bool, precPresent bool, minus bool, plus bool,
          sharp bool, space bool, zero bool, plusV bool, sharpV bool }
fmt{ buf *buffer, fmtFlags, wid int, 
     prec int, intbuf [68]byte }

常量两两一组,分别使用来标注有无符号以及处理数字加字母的大小写。

类型整体上是一个格式化器,定义了进行格式化所需的各种变量,包括:

  • buf:用来存储格式化后的内容的缓存

  • wid:输出宽度

  • prec:输出精度

  • intbuf:是一个足够大的缓存,可以存储64位有符号整形的二进制表示,或者用来避免对32位系统上的结构体末尾进行填充

  • fmtFlags:为了便于清除它,所以单独定义的结构类型

    • 它包括一系列用来判断是否存在某个格式控制符的布尔值,其中 plusV (%+v)sharpV (%#v)plus/sharp 互斥,因为它们的意义是完全不同的

方法

官方的命名还是非常清晰易懂的,我们通过名称可以判断出 fmt 的方法分为以下这四类:

  • 初始化:拷贝指向缓存的指针,并创建置零fmtFlags 结构体

  • 填充:包括向缓冲写入字符/字符串、添加空格、添加前导0、左对齐或右对齐操作

  • 精度截断:只是对字符串的截断

  • 对不同格式化类型的格式化操作:这里是每一种类型的具体的格式化操作,如何解析完整的格式化命令在 print.go

填充

我们先来看看不同的填充方法。

填充空格或前导零

func (f *fmt) writePadding(n int) {
	if n <= 0 {
		return
	}
	buf := *f.buf
	oldLen := len(buf)
	newLen := oldLen + n
	if newLen > cap(buf) {
		buf = make(buffer, cap(buf)*2+n)
		copy(buf, *f.buf)
	}
	padByte := byte(' ')
	if f.zero {
		padByte = byte('0')
	padding := buf[oldLen:newLen]
	for i := range padding {
		padding[i] = padByte
	}
	*f.buf = buf[:newLen]
}

输入要填充的长度,必要的时候对缓存进行扩容,判断是用空格还是0来填充,填充然后将缓存长度截止到填充后的新长度。

fmt.Printf("%05d", 2)  // 00002
fmt.Printf("%5d", 2)   //     2

从字节切片填充

func (f *fmt) pad(b []byte) {
	if !f.widPresent || f.wid == 0 {
		f.buf.write(b)
		return
	}
	width := f.wid - utf8.RuneCount(b)
	if !f.minus {
		f.writePadding(width)
		f.buf.write(b)
	} else {
		f.buf.write(b)
		f.writePadding(width)
	}
}

如果没有宽度要求或者宽度为0,直接向缓存中写入,(write 方法还有下面函数中的 writeString 方法是对不同传入参数调用 append 的封装,在 print.go 中)。

否则,计算要填充的宽度,并判断是右对齐(在左边填充空格)还是左对齐(在右边填充空格)。

注意有 minuszero 会被清除,也就是在左对齐时不会填充后导零,即使你在格式化参数中写了,也不会起作用。

fmt.Printf("%-05d%d", 2, 1)  // 2    1

从字符串填充

func (f *fmt) padString(s string) {
	if !f.widPresent || f.wid == 0 {
		f.buf.writeString(s)
		return
	}
	width := f.wid - utf8.RuneCountInString(s)
	if !f.minus {
		f.writePadding(width)
		f.buf.writeString(s)
	} else {
		f.buf.writeString(s)
		f.writePadding(width)
	}
}

这个函数和上面的函数除了处理的类型不同以外,几乎没有差别。

精度截断

这里写的精度截断只是对输出字符串而言,它也按传入参数类型分为对字节切片的和对字符串的,两者代码同样非常地相似,我们这里就只放前者的代码了。

func (f *fmt) truncate(b []byte) []byte {
	if f.precPresent {
		n := f.prec
		for i := 0; i < len(b); {
			n--
			if n < 0 {
				return b[:i]
			}
			wid := 1
			if b[i] >= utf8.RuneSelf {
				_, wid = utf8.DecodeRune(b[i:])
			}
			i += wid
		}
	}
	return b
}

对字节切片的操作较麻烦一些,它先获取输出的精度,然后遍历字节切片,如果已经到输出精度了直接返回,否则处理下一个字符(字符的字节数可能大于一个,需要进行判断)。

如果是字符串的话,直接用 range 语句遍历即可,要简单很多。

总结

在本篇文章中,通过阅读 format.go 的源码,我们了解了格式化器的具体结构和包含的方法。本篇文章先讲到这里,下一篇我们继续分析格式化器是如何对不同的格式化类型进行格式化操作的。

最后,如果本篇文章对您有所帮助,希望您可以 点赞、收藏、评论,感谢支持 ✧(≖ ◡ ≖✿