标准库 - fmt/format.go 解读

Posted GoLove

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了标准库 - fmt/format.go 解读相关的知识,希望对你有一定的参考价值。

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// go/src/fmt/format.go
// version 1.7

package fmt

import (
	"strconv"
	"unicode/utf8"
)

// 用于进制转换
const (
	ldigits = "0123456789abcdefx"
	udigits = "0123456789ABCDEFX"
)

// 作为参数使用,方便阅读者明白传入的参数是什么含义
const (
	signed   = true
	unsigned = false
)

// 用于记录“占位符”中是否指定了相应的标记
// 将标志放在一个单独的结构体中便于清理
type fmtFlags struct {
	widPresent  bool // 宽度值
	precPresent bool // 精度值
	minus       bool // - 标记
	plus        bool // + 标记
	sharp       bool // # 标记
	space       bool // 空格标记
	zero        bool // 0 标记

	// 对于特殊格式 %+v 和 %#v,单独设置 plusV/sharpV 标志。
	plusV  bool // +v
	sharpV bool // #v
}

// fmt 是一个原始的格式化器,用于 Printf 等函数中。
// 它将格式化结果输出到一个缓冲区中,缓冲区必须单独指定。
type fmt struct {
	buf *buffer // *[]byte

	fmtFlags    // 结构体,定义了许多标志

	wid  int    // 宽度
	prec int    // 精度

	// intbuf 足够存储二进制格式的 int64
	intbuf [68]byte
}

// 复位所有标志
func (f *fmt) clearflags() {
	f.fmtFlags = fmtFlags{}
}

// 结构体初始化(必须提供缓冲区)
func (f *fmt) init(buf *buffer) {
	f.buf = buf
	f.clearflags()
}

// 将数据写入缓冲区(仅写入 n 字节的填充字符)
func (f *fmt) writePadding(n int) {
	if n <= 0 {
		return
	}
	buf := *f.buf
	// 先做容量判断,如果容量不足,则进行扩充
	oldLen := len(buf)
	newLen := oldLen + n
	if newLen > cap(buf) {
		// 默认将容量翻倍,但要保证能够容纳写入的
		// 内容,所以要 +n
		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
	}
	// buf 有可能进行了扩充(地址发生了改变),所以要写回去
	*f.buf = buf[:newLen]
}

// 将数据写入缓冲区(写入字节切片,同时处理宽度填充)
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)
	}
}

// 将数据写入缓冲区(写入字符串,同时处理宽度填充)
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) fmt_boolean(v bool) {
	if v {
		f.padString("true")
	} else {
		f.padString("false")
	}
}

// 将数据写入缓冲区(写入 Unicode 码点)
// Unicode 码点格式为 "U+FFFF",如果指定了 # 标记,则格式为 "U+FFFF ‘相应字符‘"。
func (f *fmt) fmt_unicode(u uint64) {
	// 临时缓冲区,容量为 68 字节,如果容量不够用,可以进行进行扩充。
	buf := f.intbuf[0:]

	// 1、判断容量是否够用

	// 如果没有指定精度,那么容量肯定够用,因为即便使用 %#U 对 -1 进行格式化,
	// 所需的最大存储空间也只有 18 字节("U+FFFFFFFFFFFFFFFF"),没有超过 68。
	// 只有指定了过大的精度之后(比如 100),才有可能超出容量范围。
	// 所以下面对容量的判断,只和精度有关。

	// 默认精度为 4(如果码点长度不足 4 位,则添加前导 0,比如 U+0065,如果
	// 码点长度超过精度值,则忽略精度)
	prec := 4

	// 如果指定了精度,则判断格式化结果是否会超出 buf 范围
	if f.precPresent && f.prec > 4 {
		prec = f.prec
		// 估算所需的存储空间:"U+"、精度、" ‘"、相应字符、"‘"。
		width := 2 + prec + 2 + utf8.UTFMax + 1
		if width > len(buf) {
			buf = make([]byte, width)
		}
	}

	// 开始格式化

	// 从右向左进行格式化更容易一些
	i := len(buf)

	// 2、处理 # 标记

	// 在最后添加 ‘相应字符‘。
	// 前提是数值必须在 Unicode 码点范围内,并且字符可打印
	if f.sharp && u <= utf8.MaxRune && strconv.IsPrint(rune(u)) {
		i--
		buf[i] = ‘\‘‘
		i -= utf8.RuneLen(rune(u))
		utf8.EncodeRune(buf[i:], rune(u))
		i--
		buf[i] = ‘\‘‘
		i--
		buf[i] = ‘ ‘
	}

	// 3、将 u 格式化为十六进制数值。

	for u >= 16 {
		i--                     // 确定 buf 的写入下标
		buf[i] = udigits[u&0xF] // 与 1111 相与,获取十六进制的个位数,然后查表取字符。
		prec--                  // 精度被用掉一个
		u >>= 4                 // 丢弃十六进制的个位数
	}
	i--                         // 处理最后一个个位数
	buf[i] = udigits[u]
	prec--

	// 4、处理精度信息(添加前导 0)

	for prec > 0 {
		i--
		buf[i] = ‘0‘
		prec--
	}

	// 5、处理前导 "U+"。

	i--
	buf[i] = ‘+‘
	i--
	buf[i] = ‘U‘

	// 6、处理宽度信息(填充空格)

	oldZero := f.zero
	f.zero = false
	f.pad(buf[i:])
	f.zero = oldZero
}

// 将数据写入缓冲区(整数:包括有符号和无符号,处理进制转换)
// u:要格式化的整数。base:进制。isSigned:是否有符号。digits:进制转换表格
// 十六进制大小写通过 digits 参数确定
func (f *fmt) fmt_integer(u uint64, base int, isSigned bool, digits string) {

	// 1、修正参数

	// 如果确实有符号,则将 u 中存储的负数变为正数
	negative := isSigned && int64(u) < 0
	if negative {
		u = -u // 相当于 -int64(u),类型转换不会改变值内容,所以减哪个都一样,-u 省一步转换操作
	}

	// 临时缓冲区,容量为 68 字节,如果容量不够用,可以进行进行扩充。
	buf := f.intbuf[0:]

	// 2、判断容量是否够用

	if f.widPresent || f.precPresent {
		// 需要额外的 3 个字节来存放带符号的 "0x"。
		// 这里为了提高效率,直接将宽度和精度相加(实际上宽度和精度是重叠的),
		// 因为在大多数情况下,相加的结果都不会太大,width > len(buf) 的情况很
		// 少出现,很少会重新分配内存,所以这里只是为了确保安全而已。相反,如果
		// 用判断语句计算准确的 width 值,反而会降低效率。
		width := 3 + f.wid + f.prec
		if width > len(buf) {
			buf = make([]byte, width)
		}
	}

	// 3、确定精度信息

	// 注:有两种方式为整数添加前导零:%.3d 或 %08d,
	// 如果同时使用了这两种写法,那么 0 标记将被忽略,会使用空格实现宽度填充。

	// 默认精度为 0,如果指定了精度,则使用指定的精度。
	prec := 0
	if f.precPresent {
		prec = f.prec
		// 如果精度指定为 0,值也指定为 0,则表示无内容,只用空格进行填充。
		// 例如:fmt.Printf("%#8.d", 0)
		if prec == 0 && u == 0 {
			oldZero := f.zero
			f.zero = false
			f.writePadding(f.wid)
			f.zero = oldZero
			return
		}
	// 如果没有指定精度,但指定了 0 标记和宽度,
	// 则将宽度值转换为精度值,由精度处理函数去处理前导 0。
	} else if f.zero && f.widPresent {
		prec = f.wid
		// 如果指定了符号位,则留下一个符号位
		if negative || f.plus || f.space {
			prec--
		}
	}

	// 从右到左进行格式化更容易一些
	i := len(buf)

	// 4、开始编码

	// 使用常数进行除法和取模操作可以更有效率。
	// case 顺序按照使用频繁度排序。
	switch base {
	case 10:
		for u >= 10 {
			i--                              // 确定缓冲区的写入下标
			next := u / 10                   //去掉个位数
			// 这里用了 - 和 * 求余数,而没有用 %,是不是比 % 更快一些?
			buf[i] = byte(‘0‘ + u - next*10) // 获取个位数
			u = next
		}
	case 16:
		for u >= 16 {
			i--                    // 确定缓冲区的写入下标
			buf[i] = digits[u&0xF] // 与 1111 相与,获取十六进制的个位数,然后查表取字符。
			u >>= 4                // 丢弃十六进制的个位数
		}
	case 8:
		for u >= 8 {
			i--                      // 确定缓冲区的写入下标
			buf[i] = byte(‘0‘ + u&7) // 与 111 相与,获取八进制的个位数,然后转换为字符。
			u >>= 3                  // 丢弃八进制的个位数
		}
	case 2:
		for u >= 2 {
			i--                      // 确定缓冲区的写入下标
			buf[i] = byte(‘0‘ + u&1) // 与 1 相与,获取二进制的个位数,然后转换为字符。
			u >>= 1                  // 丢弃二进制的个位数
		}
	default:
		panic("fmt: unknown base; can‘t happen") // 未知进位制
	}
	i--                // 最后的个位数还没处理,在这里进行处理
	buf[i] = digits[u] // 所有进制的个位数都可以查表取字符。

	// 5、处理精度信息(添加前导 0)

	for i > 0 && prec > len(buf)-i {
		i--
		buf[i] = ‘0‘
	}

	// 6、处理前缀:0x、0 等

	if f.sharp {
		switch base {
		case 8:
			if buf[i] != ‘0‘ {
				i--
				buf[i] = ‘0‘
			}
		case 16:
			// 根据参数 digits 确定大小写:0x、0X
			i--
			buf[i] = digits[16]
			i--
			buf[i] = ‘0‘
		}
	}

	// 7、处理符号位

	if negative {
		i--
		buf[i] = ‘-‘
	} else if f.plus {
		i--
		buf[i] = ‘+‘
	} else if f.space {
		i--
		buf[i] = ‘ ‘
	}

	// 8、处理宽度信息(填充空格)

	oldZero := f.zero
	f.zero = false
	f.pad(buf[i:])
	f.zero = oldZero
}

// 将字符串截断到指定精度
func (f *fmt) truncate(s string) string {
	if f.precPresent {
		n := f.prec
		for i := range s {
			n--
			if n < 0 {
				return s[:i]
			}
		}
	}
	return s
}

// 将数据写入缓冲区(字符串:处理宽度和精度信息)
func (f *fmt) fmt_s(s string) {
	s = f.truncate(s)
	f.padString(s)
}

// 将数据写入缓冲区(字符串/字节切片:十六进制格式)
func (f *fmt) fmt_sbx(s string, b []byte, digits string) {

	// 1、计算结果长度

	// 获取要处理的字符串或字节切片长度
	length := len(b)
	if b == nil {
		length = len(s)
	}
	// 只处理精度范围内的内容
	if f.precPresent && f.prec < length {
		length = f.prec
	}
	// 每个元素(字节)需要 2 个字节存储其十六进制编码。
	width := 2 * length
	if width > 0 {
		if f.space {
			// 元素之间有空格,所以需要在每个元素前面都添加 0x 或 0X。
			// 每个元素的十六进制编码刚好是 2 个字节,所以乘以 2 之后,
			// 刚好每个元素多出 2 个字节的空间来存放 0x 或 0X
			if f.sharp {
				width *= 2
			}
			// 各个元素之间将被一个空格隔开
			width += length - 1
		} else if f.sharp {
			// 元素之间没有空格,只需要在开头添加一个 0x 或 0X 即可。
			width += 2
		}
	} else { // 元素为空,则仅用空格填充宽度,比如:fmt.Printf("%8x", "")
		if f.widPresent {
			f.writePadding(f.wid)
		}
		return
	}

	// 2、处理“左”宽度信息

	if f.widPresent && f.wid > width && !f.minus {
		f.writePadding(f.wid - width)
	}

	// 3、开始编码

	buf := *f.buf

	// 在第一个元素前面添加前导 0x 或 0X。
	if f.sharp {
		buf = append(buf, ‘0‘, digits[16])
	}
	// 遍历各个元素(字节)
	var c byte
	for i := 0; i < length; i++ {
		// 元素之间添加空格,每个元素前面添加 0x 或 0X。
		if f.space && i > 0 {
			buf = append(buf, ‘ ‘)
			if f.sharp {
				buf = append(buf, ‘0‘, digits[16])
			}
		}
		// 对当前元素进行编码
		if b != nil {
			c = b[i] 
		} else {
			c = s[i]
		}
		buf = append(buf, digits[c>>4], digits[c&0xF])
	}

	// 由于 append 操作,缓冲区可能被扩展
	*f.buf = buf

	// 4、处理“右”宽度信息

	if f.widPresent && f.wid > width && f.minus {
		f.writePadding(f.wid - width)
	}
}

// 将数据写入缓冲区(字符串:十六进制格式)
func (f *fmt) fmt_sx(s, digits string) {
	f.fmt_sbx(s, nil, digits)
}

// 将数据写入缓冲区(字节切片:十六进制格式)
func (f *fmt) fmt_bx(b []byte, digits string) {
	f.fmt_sbx("", b, digits)
}

// 将数据写入缓冲区(字符串:带双引号,未转义)
// 如果指定了 # 标记,而且字符串不包含任何控制字符(除制表符),
// 则会返回一个原始字符串(带反引号)。
func (f *fmt) fmt_q(s string) {

	// 1、处理精度信息

	// 将字符串截断到指定精度
	s = f.truncate(s)

	// 2、开始编码

	// 处理 # 号
	if f.sharp && strconv.CanBackquote(s) {
		f.padString("`" + s + "`")
		return
	}

	// 临时缓冲区,存放临时编码结果
	buf := f.intbuf[:0]

	// 编码并处理宽度信息
	if f.plus {
		// 非 ASCII 字符将被转换为 Unicode 码点
		f.pad(strconv.AppendQuoteToASCII(buf, s))
	} else {
		// 非 ASCII 字符将正常输出
		f.pad(strconv.AppendQuote(buf, s))
	}
}

// 将数据写入缓冲区(字符)
// 如果字符不是有效的 Unicode 编码,则写入 ‘\ufffd‘
func (f *fmt) fmt_c(c uint64) {
	r := rune(c)
	// 超出 Unicode 范围
	if c > utf8.MaxRune {
		r = utf8.RuneError
	}
	// 临时缓冲区
	buf := f.intbuf[:0]
	// 对 r 进行编码
	w := utf8.EncodeRune(buf[:utf8.UTFMax], r)
	// 将编码结果经过填充后写入缓冲区
	f.pad(buf[:w])
}

// 将数据写入缓冲区(字符:带单引号,未转义)
// 如果字符不是有效的 Unicode 编码,则写入 ‘\ufffd‘
func (f *fmt) fmt_qc(c uint64) {
	r := rune(c)
	// 超出 Unicode 范围
	if c > utf8.MaxRune {
		r = utf8.RuneError
	}
	// 临时缓冲区
	buf := f.intbuf[:0]
	// 编码并处理宽度信息
	if f.plus {
		// 非 ASCII 字符将被转换为 Unicode 码点
		f.pad(strconv.AppendQuoteRuneToASCII(buf, r))
	} else {
		// 非 ASCII 字符将被正常输出
		f.pad(strconv.AppendQuoteRune(buf, r))
	}
}

// 将数据写入缓冲区(float64)
// 如果参数所提供的动词是一个有效的格式区分符,则可以在 strconv.AppendFloat 中
// 将其转换为字节类型传入。
func (f *fmt) fmt_float(v float64, size int, verb rune, prec int) {

	// 1、开始编码

	// “占位符”中的精度将覆盖参数中的默认精度
	if f.precPresent {
		prec = f.prec
	}
	// 格式化数值,结果写入临时缓冲区,为 + 号预留一个空格
	num := strconv.AppendFloat(f.intbuf[:1], v, byte(verb), prec, size)

	// 2、处理符号

	// 如果转换结果中有符号,则去掉预留的空格,否则将预留的空格转换为 + 号
	if num[1] == ‘-‘ || num[1] == ‘+‘ {
		num = num[1:]
	} else {
		num[0] = ‘+‘
	}
	// 如果指定了空格标记,而没有指定 + 标记,则将 + 改为空格。
	if f.space && num[0] == ‘+‘ && !f.plus {
		num[0] = ‘ ‘
	}

	// 3、处理无穷大和非数字

	// 它看起来不像一个数字,所以不应该用 0 填充。
	if num[1] == ‘I‘ || num[1] == ‘N‘ {
		oldZero := f.zero
		f.zero = false
		// 如果没有指定符号,则移除 NaN 前面的符号
		if num[1] == ‘N‘ && !f.space && !f.plus {
			num = num[1:]
		}
		f.pad(num)
		f.zero = oldZero
		return
	}

	// 4、处理填充后写入 f.buf

	// 指定了 + 号 || 未指定 + 号,且结果不是以 + 开头。
	if f.plus || num[0] != ‘+‘ {
		// 如果用 0 进行了左填充,那么我们希望符号在所有 0 的前面。
		if f.zero && f.widPresent && f.wid > len(num) {
			f.buf.WriteByte(num[0])          // 写入符号
			f.writePadding(f.wid - len(num)) // 进行填充
			f.buf.Write(num[1:])             // 写入符号之外的内容
			return
		}
		f.pad(num)
		return
	}
	// 未指定 + 号,但结果是以 + 开头,则去掉 + 号。
	f.pad(num[1:])
}



以上是关于标准库 - fmt/format.go 解读的主要内容,如果未能解决你的问题,请参考以下文章

python语言线程标准库threading.local源码解读

标准库 - unicode/utf8/utf8.go 解读

Go标准库:Go template 用法解读

Go标准库:Go template 用法解读

golang bytes包解读

Kotlin标准库函数 ① ( apply 标准库函数 | let 标准库函数 )