



常量是现代编程语言中最常见的语法元素。在类型系统十分严格的Go语言中,常量还兼具特殊的作用,这一条将介绍Go常量究竟能给我们日常的Go编码提供哪些帮助。
先来回顾一下C语言。在C语言中,字面值(literal)担负着常量的角色(针对整型值,还可以使用枚举常量)。可以使用整型、浮点型、字符串型、字符型字面值来满足不同场合下对常量的需求:
0x12345678 10086 3.1415926 "Hello, Gopher" 'a'
为了不让这些魔数(magic number)充斥于源码各处,早期C语言的常用实践是使用宏(macro)定义记号来指代这些字面值:
#define MAX_LEN 0x12345678 #define CMCC_SERVICE_PHONE_NUMBER 10086 #define PI 3.1415926 #define WELCOME_TO_GO "Hello, Gopher" #define A_CHAR 'a'
这种定义“具名字面值”的实践也被称为宏定义常量。虽然后续的C标准中提供了const关键字来定义在程序运行过程中不可改变的变量(又称“只读变量”),但使用宏定义常量的习惯依然被沿袭下来,并且依旧是C编码中的主流风格。
宏定义的常量有着诸多不足,比如:
■仅是预编译阶段进行替换的字面值,继承了宏替换的复杂性和易错性;
■是类型不安全的;
■无法在调试时通过宏名字输出常量的值。
而C语言中const修饰的标识符本质上还是变量,和其他变量一样,编译器不能像对待真正的常量那样对其进行代码优化,也无法将其作为数组声明时的初始长度。
Go语言是站在C语言等编程语言的肩膀之上诞生的,它原生提供常量定义的关键字const。 Go语言中的const整合了C语言中宏定义常量、const只读变量和枚举常量三种形式,并消除了每种形式的不足,使得Go常量成为类型安全且对编译器优化友好的语法元素。 Go中所有与常量有关的声明都通过const来进行,例如:
// $GOROOT/src/os/file.go const ( O_RDONLY int = syscall.O_RDONLY O_WRONLY int = syscall.O_WRONLY O_RDWR int = syscall.O_RDWR O_APPEND int = syscall.O_APPEND ... )
上面这段标准库中的代码通过const声明了一组常量,如果非要进一步细分,可以将这组常量视为枚举的整型常量。然而你可能没想到,上面对常量的声明方式 仅仅是Go标准库中的少数个例 ,绝大多数情况下,Go常量在声明时并不显式指定类型,也就是说使用的是 无类型常量(untyped constant) 。比如:
// $GOROOT/src/io/io.go const ( SeekStart = 0 SeekCurrent = 1 SeekEnd = 2 )
无类型常量是Go语言在语法设计方面的一个“微创新”,也是“追求简单”设计哲学的又一体现,它可以让你的Go代码更加简洁。接下来我们就来看看无类型常量是如何简化Go代码编写的。
Go是对类型安全要求十分严格的编程语言。Go要求,两个类型即便拥有相同的底层类型(underlying type),也仍然是不同的数据类型,不可以被相互比较或混在一个表达式中进行运算:
type myInt int
func main() {
var a int = 5
var b myInt = 6
fmt.Println(a + b) // 编译器会给出错误提示:invalid operation: a + b (mismatched types int and myInt)
}
我们看到,Go在处理不同类型的变量间的运算时不支持隐式的类型转换。Go的设计者认为,隐式转换带来的便利性不足以抵消其带来的诸多问题 [1] 。要解决上面的编译错误,必须进行显式类型转换:
type myInt int
func main() {
var a int = 5
var b myInt = 6
fmt.Println(a + int(b)) // 输出:11
}
而将有类型常量与变量混合在一起进行运算求值时也要遵循这一要求,即如果有类型常量与变量的类型不同,那么混合运算的求值操作会报错:
type myInt int
const n myInt = 13
const m int = n + 5 // 编译器错误提示:cannot use n + 5 (type myInt) as type int in const initializer
func main() {
var a int = 5
fmt.Println(a + n) // 编译器错误提示:invalid operation: a + n (mismatched types int and myInt)
}
唯有进行显式类型转换才能让上面的代码正常工作:
type myInt int
const n myInt = 13
const m int = int(n) + 5
func main() {
var a int = 5
fmt.Println(a + int(n)) // 输出:18
}
有类型常量给代码简化带来了麻烦,但这也是Go语言对类型安全严格要求的结果。
[1] https://tip.golang.org/doc/faq#conversions
现在有下面这些字面值:
5 3.1415926 "Hello, Gopher" 'a' false
我们从中挑选三个字面值以魔数的形式直接参与变量赋值运算:
type myInt int
type myFloat float32
type myString string
func main() {
var j myInt = 5
var f myFloat = 3.1415926
var str myString = "Hello, Gopher"
fmt.Println(j) // 输出:5
fmt.Println(f) // 输出:3.1415926
fmt.Println(str) // 输出:Hello, Gopher
}
可以看到这三个字面值无须显式类型转换就可以直接赋值给对应的三个自定义类型的变量,这等价于下面的代码:
var j myInt = myInt(5)
var f myFloat = myFloat(3.1415926)
var str myString = myString("Hello, Gopher")
但显然之前的无须显式类型转换的代码更为简洁。
Go的无类型常量恰恰就拥有像字面值这样的特性,该特性使得无类型常量在参与变量赋值和计算过程时无须显式类型转换,从而达到简化代码的目的:
const (
a = 5
pi = 3.1415926
s = "Hello, Gopher"
c = 'a'
b = false
)
type myInt int
type myFloat float32
type myString string
func main() {
var j myInt = a
var f myFloat = pi
var str myString = s
var e float64 = a + pi
fmt.Println(j) // 输出:5
fmt.Println(f) // 输出:3.1415926
fmt.Println(str) // 输出:Hello, Gopher
fmt.Printf("%T, %v\n", e, e) // float64, 8.1415926
}
无类型常量使得Go在处理表达式混合数据类型运算时具有较大的灵活性,代码编写也有所简化,我们无须再在求值表达式中做任何显式类型转换了。
除此之外,无类型常量也拥有自己的默认类型:无类型的布尔型常量、整数常量、字符常量、浮点数常量、复数常量、字符串常量对应的默认类型分别为bool、int、int32(rune)、float64、complex128和string。当常量被赋值给无类型变量、接口变量时,常量的默认类型对于确定无类型变量的类型及接口对应的动态类型是至关重要的。示例如下。
const (
a = 5
s = "Hello, Gopher"
)
func main() {
n := a
var i interface{} = a
fmt.Printf("%T\n", n) // 输出:int
fmt.Printf("%T\n", i) // 输出:int
i = s
fmt.Printf("%T\n", i) // 输出:string
}
所有常量表达式的求值计算都可以在编译期而不是在运行期完成,这样既可以减少运行时的工作,也能方便编译器进行编译优化。当操作数是常量时,在编译时也能发现一些运行时的错误,例如整数除零、字符串索引越界等。
无类型常量是Go语言推荐的实践,它拥有和字面值一样的灵活特性,可以直接用于更多的表达式而不需要进行显式类型转换,从而简化了代码编写。此外,按照Go官方语言规范 [1] 的描述,数值型无类型常量可以提供比基础类型更高精度的算术运算,至少有256 bit的运算精度。