5 无类型常量让代码更简化
我们先来回顾一下 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 (
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // open the file read-only.
O_WRONLY int = syscall.O_WRONLY // open the file write-only.
O_RDWR int = syscall.O_RDWR // open the file read-write.
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // append data to the file when writing.
O_CREATE int = syscall.O_CREAT // create a new file if none exists.
O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist.
O_SYNC int = syscall.O_SYNC // open for synchronous I/O.
O_TRUNC int = syscall.O_TRUNC // truncate regular writable file when opened.
)
上面这段标准库中的代码通过 const 声明了一组常量,如果非要进一步细分,可以将这组常量视为枚举的整型常量。然而你可能没想到,上面对常量的声明方式仅仅是 Go 标准库中的少数个例,绝大多数情况下,Go 常量在声明时并不显式指定类型,也就是说使用的是无类型常量(untyped constants)。比如:
// $GOROOT/src/io/io.go
// Seek whence values.
const (
SeekStart = 0 // seek relative to the origin of the file
SeekCurrent = 1 // seek relative to the current offset
SeekEnd = 2 // seek relative to the end
)
无类型常量是 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 语言对类型安全严格要求的结果。
我们现在有下面这些字面值:
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
fmt.Println(j) // 输出:5
fmt.Println(f) // 输出:3.1415926
fmt.Println(str) // 输出:Hello, Gopher
}
我们看到无类型常量使得在 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 官方语言规范2描述,数值型无类型常量还可以提供比基础类型更高精度的算术运算,你可以认为至少有 256bit 的运算精度。