在Go语言中,常量表达式是在编译器求值的,因此在程序运行时是没有性能损耗的。常量的底层类型是前面提过的基本类型:布尔值,字符串,数值变量。
常量的声明方式和变量很相似,但是常量的值是不可变的,因此在运行期是不可以对常量进行修改的。例如,对于π这种数学常数,常量显然比变量更适合,因为我们不允许这个值发生任何变化:
const pi = 3.14159 // approximately; math.Pi is a better approximation
可以同时声明多个常量:
const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
常量的相关运算也是在编译期完成的,这样不仅可以做相应的编译优化,也可以提升运行时的性能。如果一个表达式的操作数是常量,那么一些运行时的错误就可以提前在编译期发现:整数除以零、字符串索引越界、浮点数计算导致的正负无穷等等。
常量作为操作数时,以下表达式的结果都是常量:算术、逻辑、比较运算,类型转换,len、cap、real、imag、comlex、unsafe.Sizeof。
因为常量是在编译器确定的,因此可以作为一些类型的组成部分,比如数组类型的长度:
const IPv4Len = 4
// parseIPv4函数对IPv4地址(d.d.d.d)进行解析.
func parseIPv4(s string) IP {
var p [IPv4Len]byte
// ...
}
常量声明时可以指定类型,也可以不指定类型,如果不指定,那么编译器会自己进行类型推断。下面代码中,time.Duration是一个具名类型,底层类型是int64,其中time.Minute是一个time.Duration类型的常量。下面声明的两个常量的类型都是time.Duration,我们可以在fmt中使用%T参数打印类型信息:
const noDelay time.Duration = 0
const timeout = 5 * time.Minute
fmt.Printf("%T %[1]v\n", noDelay) // "time.Duration 0"
fmt.Printf("%T %[1]v\n", timeout) // "time.Duration 5m0s"
fmt.Printf("%T %[1]v\n", time.Minute) // "time.Duration 1m0s"
批量声明常量时,除了第一个常量,其它常量声明的右边表达式都可以省略。如果某个常量的右边表达式缺失,则该常量的值和类型等于前面常量的值和类型,例如:
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
实际场景中,上面的代码并没有太多实用价值。但是我们可以利用它实现下面的常量的iota语法。
我们可以使用iota语法来声明一组按照同样规则初始化的常量,优点是不用每行声明都写一遍初始化语句。在一组const声明中,第一个声明的常量的iota值被设置为0,然后接下来每一个行的常量值都会递增1。
下面这个例子来自time包,首先定义了Weekday具名类型,然后定义了一组常量(一周七天),其中周日的值为0,后面的值依次递增。在C语言中,这种被称为枚举类型(Enum):
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
周日到周一的值依次是0到6。
下面是一个更为复杂的例子,来自net包, 它给一个无符号整数的5个低位bit各起了一个名字,用来做基于bit的布尔判断:
type Flags uint
const (
FlagUp Flags = 1 << iota // is up
FlagBroadcast // supports broadcast access capability
FlagLoopback // is a loopback interface
FlagPointToPoint // belongs to a point-to-point link
FlagMulticast // supports multicast access capability
)
随着iota的递增,每个常量相应的bit位都会设置为1(位左移),第一个常量为00000001,第二常量为00000010,依次类推。可以使用这些常量用于测试、设置或清除对应bit位的值,也可以用来判断某个值对应的bit是否设置为1(代表着相应的Flag是否设置)。
func IsUp(v Flags) bool { return v&FlagUp == FlagUp }
func TurnDown(v *Flags) { *v &^= FlagUp }
func SetBroadcast(v *Flags) { *v |= FlagBroadcast }
func IsCast(v Flags) bool { return v&(FlagBroadcast|FlagMulticast) != 0 }
unc main() {
var v Flags = FlagMulticast | FlagUp
fmt.Printf("%b %t\n", v, IsUp(v)) // "10001 true"
TurnDown(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10000 false"
SetBroadcast(&v)
fmt.Printf("%b %t\n", v, IsUp(v)) // "10010 false"
fmt.Printf("%b %t\n", v, IsCast(v)) // "10010 true"
}
下面的示例中,每个常量都是1024的幂:
const (
_ = 1 << (10 * iota)
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)
不过iota常量也有其局限性。例如,1000的幂就无法用iota实现,因为Go语言没有幂运算符(只能通过标准库)。
练习 3.13: 利用尽可能简洁的方式声明KB至YB之间的常量
fmt.Println(YiB/ZiB) // "1024"
再看一个例子,math.Pi是无类型的浮点数常量,可直接用在任意需要浮点数或复数的地方:
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
如果math.Pi不是无类型的而是float64类型的,那么最终结果的精度可能不同,同时从浮点数转为复数时需要显示的类型转换:
const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)
不同的常量值写法会对应不同的类型,虽然0、0.0、0i及'\u0000'有相同的常量值,但是它们分别是:无类型整数、无类型浮点数、无类型复数和无类型rune。同样,true、false是无类型布尔值,字符串值是无类型字符串。
之前的章节提过:/ 运算符会根据操作数类型生成对应的结果(整形或浮点),常量的除法也有这样的特性:
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
只有常量才能没有类型,当无类型常量被赋值给变量时,如果转换合法,那么会进行隐式的类型转换:
var f float64 = 3 + 0i // untyped complex -> float64
f = 2 // untyped integer -> float64
f = 1e123 // untyped floating-point -> float64
f = 'a' // untyped rune -> float64
上面的语句相当于:
var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')
无论隐式或者显式转换,将类型A转为类型B都需要B可以表示A代表的值。同时支持四舍五入:
const (
deadbeef = 0xdeadbeef // untyped int with value 3735928559
a = uint32(deadbeef) // uint32 with value 3735928559
b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
c = float64(deadbeef) // float64 with value 3735928559 (exact)
d = int32(deadbeef) // compile error: constant overflows int32
e = float64(1e309) // compile error: constant overflows float64
f = uint(-1) // compile error: constant underflows uint
)
在无类型的变量声明中(包含短声明),无类型常量值会被隐式转为相应的类型,例如:
i := 0 // untyped integer; implicit int(0)
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating-point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)
上面的隐式转换是有规则的:无类型整数默认转为int,无类型浮点数和复数默认转为float64和complex128。因此如果要给变量一个不同的类型,必须进行显式类型转换:
var i = int8(0)
var i int8 = 0
将无类型常量转为一个接口值时,这种默认类型就很重要,因为这样才能确定接口的动态类型(见第6章)。下面例子中,fmt接收的是接口值inteface{}参数,当把常量直接进行传参时,常量的默认类型就会成为接口值的动态类型。
fmt.Printf("%T\n", 0) // "int"
fmt.Printf("%T\n", 0.0) // "float64"
fmt.Printf("%T\n", 0i) // "complex128"
fmt.Printf("%T\n", '\000') // "int32" (rune)
现在我们已经学习了Go语言中的所有基本类型。下面的章节将学习如果使用基本类型组合成复杂数据类型,然后解决实际编程问题。