在Go语言中,常量表达式是在编译期求值的,因此在程序运行时是没有性能损耗的。常量的底层类型是前面提过的基本类型:布尔值,字符串,数值变量。
常量的声明方式和变量很相似,但是常量的值是不可变的,因此在运行期是不可以对常量进行修改的。例如,对于π这种数学常数,常量显然比变量更适合,因为我们不允许这个值发生任何变化:
const pi = 3.14159 // 近似值;实际应用请使用math.Pi,更精确
可以同时声明多个常量:
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语法。
3.6.1. 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包,每个Flag常量都是一个无符号整数:整数的指定bit被设置为1:
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(位左移),这里第一个常量的二进制为0000 0001,第二个常量的二进制为0000 0010,第三个0000 0100,依次类推。可以使用这些常量用于测试、设置或清除对应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 (超过了int32的范围)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (超过了int64的范围)
YiB // 1208925819614629174706176
)
不过iota常量也有其局限性。例如,1000的幂就无法用iota实现,因为Go语言没有幂运算符(只能通过标准库)。
练习 3.13: 利用尽可能简洁的方式声明KB至YB之间的常量
3.6.2. 无类型常量
fmt.Println(YiB/ZiB) // "1024"
再看一个例子,math.Pi是无类型浮点数常量,可直接用在任意需要浮点数或复数的地方:
var x float32 = math.Pi // 精度损耗,256bit -> 32bit
var y float64 = math.Pi // 精度损耗,256bit -> 64bit
var z complex128 = math.Pi
如果math.Pi不是无类型的而是float64类型的,那么最终结果的精度可能不同,同时从浮点数转为复数时需要显示的类型转换:
const Pi64 float64 = math.Pi // 精度损耗,256bit -> 64bit
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)
不同的常量值写法会对应不同的默认类型,虽然0、0.0、0i及'\u0000'有相同的常量值,但是它们分别是:无类型整数、无类型浮点数、无类型复数和无类型rune。同样的,true、false是无类型布尔常量,字符串值"hello"是无类型字符常量。
之前的章节提过:/ 运算符会根据操作数类型生成对应类型的结果(整形或浮点),常量的除法也有这样的特性:
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 是float64类型
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 是无类型整数, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0是无类型浮点数
只有常量才能没有类型,当无类型常量被赋值给变量时,如果转换合法,那么会进行隐式类型转换:
var f float64 = 3 + 0i // 无类型复数 -> float64
f = 2 // 无类型整数 -> float64
f = 1e123 // 无类型浮点数 -> float64
f = 'a' // 无类型rune -> float64
上面的语句相当于:
var f float64 = float64(3 + 0i)
f = float64(2)
f = float64(1e123)
f = float64('a')
无论隐式或者显式转换,将类型A转为类型B都需要B可以表示A代表的值。同时支持四舍五入:
const (
deadbeef = 0xdeadbeef // 无类型整数, 3735928559
a = uint32(deadbeef) // uint32整数, 3735928559
b = float32(deadbeef) // float32, 3735928576 (不精确)
c = float64(deadbeef) // float64 ,3735928559 (精确)
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 // 无类型整数; 隐式转换 int(0)
r := '\000' // 无类型rune; 隐式转换 rune('\000')
f := 0.0 // 无类型浮点数; 隐式转换 float64(0.0)
c := 0i // 无类型复数; 隐式转换 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语言中的所有基本类型。下面的章节将学习如果使用基本类型组合成复杂数据类型,然后解决实际编程问题。
文章所有权:Golang隐修会 联系人:孙飞,CTO@188.com!
有疑问加站长微信联系(非本文作者)