常量是在编译期就确定的值。用 const 声明它,Go 会拒绝任何修改它的尝试。
常量可以在包级别或函数内部声明,可以每行一个,也可以放在 const ( ... ) 块中。MB 和 GB 由其他常量计算得到,这些运算由编译器在构建时完成一次。
给常量赋值(例如 MaxRetries = 5)是编译错误:
./main.go:8:2: cannot assign to MaxRetries (neither addressable nor a map index expression)
哪些值可以是常量
只有三类值:布尔值、数字(整数、浮点数、复数和 rune)以及字符串。值必须能由编译器算出,也就是字面量、其他常量、对它们的算术运算,以及少数内置函数,比如对常量字符串取 len。
任何需要运行代码或分配内存的东西都不行:
const Colors = []string{"red", "green"} // slice
const Started = time.Now() // function call
./main.go:9:16: []string{…} (value of type []string) is not constant
./main.go:10:13: time.Now() (value of struct type time.Time) is not constant
为什么 Go 没有常量切片或 map
切片、map 以及包含它们的结构体都是对运行时内存的引用,所以编译器无法把它们作为固定值写进程序,而 Go 也没有用于变量的 readonly 或 final 修饰符。常见的变通办法:
每次调用都会产生一次内存分配,对于配置这种规模的数据来说没有问题。如果要按键查找,函数中的 switch 是另一种类似常量的选择:func statusText(code int) string { switch code { ... } }。
无类型常量
声明时没有指定类型的常量是无类型的。它有一个种类(整数、浮点数、rune、字符串、布尔值),但在被使用之前没有具体的 Go 类型,会根据上下文自动适配:
Ratio 不需要转换就能当作 int、float64、uint8 和 float32 使用。这就是 time.Sleep(2 * time.Second) 能通过编译的原因:2 是无类型的,会变成 time.Duration。
当上下文不要求特定类型时,例如 x := Ratio,常量会使用它的默认类型:
| 无类型常量的种类 | 默认类型 |
|---|---|
整数(42) | int |
浮点数(4.2) | float64 |
rune('a') | rune(int32) |
复数(2i) | complex128 |
| 字符串 | string |
| 布尔值 | bool |
精确运算
无类型数值常量是精确的。编译器至少用 256 位来表示整数,所以中间值可以远远超出任何 Go 类型的范围:
Huge 本身不能作为整数打印,因为把它传给 fmt.Println 会把它转换成 int,编译器会发现这个问题(float64(Huge) 可以,因为浮点数能容纳这么大的数量级):
cannot use Huge (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows)
同样的检查也能在编译期拦住更小的错误,比如 var b byte = 300:
cannot use 300 (untyped int constant) as byte value in variable declaration (overflows)
有类型常量
给常量指定类型后,它在任何地方的行为都和该类型的值一样,包括 Go 不允许混用类型这一点:
const Limit int = 10
var f float64 = 2
fmt.Println(Limit * f)
invalid operation: Limit * f (mismatched types int and float64)
如果写成 const Limit = 10(无类型),Limit * f 就能通过编译,结果是 20。所以除非类型本身有意义,否则让常量保持无类型。当你声明属于自定义命名类型的常量时,类型就有意义了,Go 正是这样实现枚举的:
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
)
这里类型才是重点:接受 Weekday 的函数清楚地说明了它需要什么。iota 以及基于它的枚举写法会在枚举和 iota 页面中讲解。
const 与 var
const | var | |
|---|---|---|
| 能否改变 | 不能 | 能 |
| 值何时确定 | 编译期 | 运行时 |
| 允许的类型 | 布尔、数值、字符串 | 任意类型 |
| 运行时占用内存 | 否 | 是 |
能否取地址(&x) | 不能 | 能 |
| 未使用时是否报错 | 否 | 是(局部变量) |
真正固定不变的东西都用 const:上限、大小、名称、协议代码、格式字符串。你会免费获得编译期溢出检查和精确运算。
易错点
有类型常量不会自动适配。 const Timeout int = 5 不经转换就不能传给需要 time.Duration 的地方;无类型的 const Timeout = 5 则可以直接相乘:Timeout * time.Second。
整数常量做的是整数除法。 const Half = 1 / 2 的结果是 0,因为两个操作数都是无类型整数。写成 1.0 / 2 才能得到 0.5。
不能取地址。 &MaxRetries 无法通过编译。如果某个 API 需要 *int,先把常量复制到一个变量中。
常见问题
Go 中如何声明常量?
使用 const:const MaxRetries = 3,或者显式指定类型:const Timeout time.Duration = 5 * time.Second。多个常量可以放在一个块中:const ( A = 1; B = 2 )。值必须在编译期就能算出来。
Go 中可以定义常量切片、map 或数组吗?
不可以。常量只能是布尔值、数字(包括 rune)和字符串。const Colors = []string{"red"} 会报错 is not constant。请改用包级别的 var;如果调用方不能修改它,就提供一个返回新副本的函数。
Go 中的无类型常量是什么?
声明时没有指定类型的常量,例如 const Pi = 3.14159。在被使用之前它没有固定的类型,所以同一个常量可以当作 float32、float64 使用,在值允许的情况下也可以当作 int。无类型常量还是精确的:编译器至少保留 256 位精度,所以只要用到它的表达式结果放得下,const Big = 1 << 100 就是合法的。
Go 中 const 和 var 有什么区别?
const 在编译期就已确定,永远不能改变;它在运行时不占内存,只能保存布尔值、数字或字符串。var 是一块可以重新赋值的内存,可以保存任何类型,包括运行时才算出来的值,例如 time.Now()。