go语言的常量处理 | 青训营笔记

70 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 7 天

go语言中的常量与一些语言中的常量有共同点但也有一些比较有趣的区别。go语言中的常量不像Java中的final只是代表不可变值和部分编译优化,go语言中的常量都必须是能在编译期确定和计算的值和类型,并通过一些形式填写到用到它们的位置,不像C++中的const会在运行时存在内存地址,go中的常量无法进行取地址操作,它更像是C++中的constexpr。go语言中的常量可以与iota配合完成常量的自动化连续赋值,可以用于达到类似枚举类的效果,弥补了go语言中没有枚举类的语法的问题。同时,由于go语言中的常量都是在编译期完成计算的,其底层的数据类型必须是基本数据类型,但是可以包括通过type定义的成别的类型的基本数据类型。在go语言中没有指明类型的常量的数字数据精度是比较大的,可以认为有至少256bit的精度典型的实现一般使用精度为512bit的,可以进行一些大整数大浮点数的计算。

go语言的源码中关于常量计算的代码在GOROOT/src/go/constant中,在源码中就可以看出为什么go中的常量的数值计算可以有较高的精度了。在源码中计算值的value.go中定义的值类型对于能用int64表示的常量值使用的类型是int64Val而对于不能用int64表示的常量值使用的类型是intValratValfloatValcomplexVal其对应的类型分别为struct{ val *big.Int }struct{ val *big.Rat }struct{ val *big.Float }struct{ re, im Value }。因为使用了go标准库中的高精度包计算这些值,因此这些值也就有了相应的精度。其中value.go中定义的了接口Value,其定义了一系列方法,以方便得到相应值类的具体类型等一些如变成字符串的常用操作。

// A Value represents the value of a Go constant.
type Value interface {
	// Kind returns the value kind.
	Kind() Kind

	// String returns a short, quoted (human-readable) form of the value.
	// For numeric values, the result may be an approximation;
	// for String values the result may be a shortened string.
	// Use ExactString for a string representing a value exactly.
	String() string

	// ExactString returns an exact, quoted (human-readable) form of the value.
	// If the Value is of Kind String, use StringVal to obtain the unquoted string.
	ExactString() string

	// Prevent external implementations.
	// 通过这个方法屏蔽外部实现,实现了该接口的类型的这个
        // 方法都是nop方法
	implementsValue()
}

同时比较有趣的一点是go中常量的字符串是通过惰性求值得到最后的常量值结果的。通过一个二叉树形结构保存字符串的拼接结构,并通过加锁保证求值只进行一次。字符串常量的值类型为stringVal其是一个结构体。

type stringVal  struct {
	// l和r要么都为nil但是s有结果,否则l和r都不为nil
	mu   sync.Mutex
	s    string
	l, r *stringVal
}

字符串常量支持的计算操作只支持+

case *stringVal:
	if op == token.ADD {
		return &stringVal{l: x, r: y.(*stringVal)}
	}

在计算一个字符串常量的具体结果值时,就会对字符串的树形结构进行具体的计算并得到结果,这部分的计算在stringValstring方法中。

// 叶节点
// MakeString returns the String value for s.
func MakeString(s string) Value { return &stringVal{s: s} }

func (x *stringVal) string() string {
	x.mu.Lock()
	if x.l != nil {
		x.s = strings.Join(reverse(x.appendReverse(nil)), "")
		x.l = nil
		x.r = nil
	}
	s := x.s
	x.mu.Unlock()

	return s
}

// 数组切片反序
func reverse(x []string) []string {
	n := len(x)
	for i := 0; i+i < n; i++ {
		x[i], x[n-1-i] = x[n-1-i], x[i]
	}
	return x
}

// 将所有字符串常量的字串以反序的形式放入切片中,
// 反序存储这些子串可以减少递归调用进行求值的层数
// ((((a + b) + c) + d) + e)
func (x *stringVal) appendReverse(list []string) []string {
	y := x
	for y.r != nil { // 如果已经是合并好的子节点或者是叶节点,直接取s即可
		y.r.mu.Lock()
                // 将子节点的右子节点的所有子串反序加入
		list = y.r.appendReverse(list)
		y.r.mu.Unlock()

		l := y.l
		if y != x {
			y.mu.Unlock()
		}
		l.mu.Lock()
                // 对左子节点的右孙子节点同样做上面的操作
		y = l
	}
        // 加入剩下的最左侧子串
	s := y.s
	if y != x {
		y.mu.Unlock()
	}
	return append(list, s)
}