Go语言核心之美 2.6-常量

在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. 无类型常量


Go语言中的常量有一点很特别:虽然一个常量可以指定某个特定的基本类型,例如int或float64或者类似time.Duration这样的具名基本类型,但是在实际应用中很多常量声明时都不指定类型。编译器表示无类型常量时用的精度比表示基本类型更高,同时无类型常量的算术运算也会更加精确,你可以假定这种精度至少是256bit。这里有六种无类型常量:无类型布尔值,无类型整数、无类型rune、无类型浮点数、无类型复数以及无类型字符串。
无类型常量不仅可以提供更高的精度,而且可以在表达式中避免显示类型转换。例如,上面例子中的ZiB、YiB的值已经超过Go语言中任何整数类型所能表达的范围,但是它们依然是合法的常量。也可以像下面这样使用:

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!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值