目标
概念
- 常量与变量的主要区别在于:
- 不可变性:常量在声明后其值就固定下来,不能再被修改。这保证了程序运行时不会因意外修改而导致错误。
- 使用不可变数据(例如数学常数 π)可以避免意外修改带来的问题
- 编译期计算:常量表达式在编译期间就会被求值,这不仅减少了运行时的计算负担,还意味着许多在运行时可能出错的操作(比如除零、越界)都可以在编译阶段捕获。
- 不可变性:常量在声明后其值就固定下来,不能再被修改。这保证了程序运行时不会因意外修改而导致错误。
- Go 中常量的潜在类型为布尔型、字符串或数字
- 无类型常量:在定义时没有显式指定类型的常量。比如写
3.14
、0
或"hello"
,这些数值或字符串常量在初始状态下没有固定的类型。 - 六种无类型常量:分别对应布尔、整数、字符(rune)、浮点数、复数和字符串。
- 无类型常量更高精度的运算:无类型常量在算术运算中拥有比具体类型(如 int 或 float64)更高的精度(可以看作有 256 位或更多位的精度);像 ZiB 或 YiB 这样超出任何内置整数类型范围的常量,也仍然可以参与运算(例如
YiB / ZiB
),因为在编译期编译器会将它们按照数学上准确的值处理。
要点
常量声明
-
声明常量时,可以直接赋予初值:
const pi = 3.14159 // 近似值;实际上 math.Pi 提供了更精确的值
这和变量声明语法类似,但常量的值一旦设定就不能修改。
-
批量声明常量
多个相关常量可以使用小括号一起声明:
const (e = 2.71828182845904523536028747135266249775724709369995957496696763pi = 3.14159265358979323846264338327950288419716939937510582097494459 )
这样的批量声明不仅代码更简洁,也可以利用类型推断:如果没有显式指定类型,常量的类型会根据右侧的表达式自动推断。
类型推断说明
-
如果你在常量声明时没有显式标明类型,则 Go 会根据右边的表达式推断类型。例如:
const timeout = 5 * time.Minute
此时
timeout
会被推断为time.Duration
类型,因为time.Minute
本身是time.Duration
类型的常量。
-
编译期常量表达式与优化
在数组定义中,可以用常量来指定数组的长度:
const IPv4Len = 4func parseIPv4(s string) IP {var p [IPv4Len]byte// 解析 IPv4 地址的逻辑
}
这样,数组长度在编译期间就确定下来了。程序运行时减少了不必要的计算,提高了效率。
iota常量生成器
-
iota的基本原理
- 自动递增:在一个
const
声明块中,iota 是一个预定义标识符,它在第一行被置为 0,后续每出现一行常量声明,它的值自动加 1。 - 省略初始化表达式:如果在常量声明块中,后面的常量省略了右侧的表达式,那么它们将默认使用前一行的表达式。这样在简单的复制中,虽然没有太大实用价值,但在配合 iota 时可以生成有规律的值。
- 自动递增:在一个
-
例子
type Weekday intconst (Sunday Weekday = iota // 0Monday // 1Tuesday // 2Wednesday // 3Thursday // 4Friday // 5Saturday // 6 )
这种方式使得常量的赋值变得简单且易于维护。
-
iota 还常用于生成一系列位掩码(bit mask)
type Flags uintconst (FlagUp Flags = 1 << iota // 1 << 0 = 1 (第 0 位)FlagBroadcast // 1 << 1 = 2 (第 1 位)FlagLoopback // 1 << 2 = 4 (第 2 位)FlagPointToPoint // 1 << 3 = 8 (第 3 位)FlagMulticast // 1 << 4 = 16 (第 4 位) )
每个常量都代表一个单独的 bit 位,这样在设置或测试标志时,可以使用位运算:
- 测试标志:
v & FlagUp == FlagUp
- 清除标志:
v &^= FlagUp
—— 把 v 中与 FlagUp 对应的那一位变成 0(不管原来是1还是0)。这样就清除了这个标志。 - 设置标志:
v |= FlagBroadcast
—— 把 v 中与 FlagBroadcast 对应的那一位变成1(即使之前是0)
- 测试标志:
-
利用 iota 生成一系列以 1024 为底的幂(例如 KiB、MiB 等)
const (_ = 1 << (10 * iota)KiB // 1 << 10 = 1024MiB // 1 << 20 = 1048576GiB // 1 << 30 = 1073741824TiB // 1 << 40 = 1099511627776PiB // 1 << 50 = 1125899906842624EiB // 1 << 60 = 1152921504606846976// ZiB, YiB 等可能会超过某些平台的位数限制 )
通过这种方式,编译器自动计算出每一项的值,而无需手动写出复杂的计算。
-
局限性
不能产生任意的幂:例如,要生成 1000、1000000 等(通常用于 KB、MB 等),因为 Go 语言没有内置的幂运算符(如
**
或pow
),所以不能直接利用 iota 来生成 10 的幂。
无类型常量的灵活性:隐式转换
当你将无类型常量赋值给一个变量时,Go 编译器会根据上下文自动推断出一个“默认类型”。例如:
-
var x float64 = math.Pi
这里,math.Pi
是一个无类型浮点常量,赋值时自动转换成 float64。 -
又如:
i := 0 // 隐式为 int 类型 f := 0.0 // 隐式为 float64 类型 c := 0i // 隐式为 complex128 类型 r := '\000'// 隐式为 rune(int32)类型
对于一个没有显式类型的变量声明(包括简短变量声明),常量的形式将隐式决定变量的默认类型,
这种机制让我们在书写表达式时更灵活,无需反复写类型转换代码。
隐式转换与默认类型
当无类型常量出现在需要具体类型的上下文时(比如当一个无类型的常量被赋值给一个变量的时候),编译器会自动进行转换。例如:
var f float64 = 3 + 0i // 这里 3+0i 是无类型复数,但可以隐式转换为 float64
这种转换类似于在后台写了:
var f float64 = float64(3 + 0i)
注意: 转换要求目标类型必须能表示原始常量的值。如果值太大或不适合,则会导致编译错误。例如:
- 将一个超出 int32 范围的无类型整数转换为 int32,会报错;
- 将一个超出 float64 表示范围的浮点数转换为 float64,同样会报错。
无类型常量隐式转换的更多例子
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // 输出 "100"
fmt.Println(5 / 9 * (f - 32)) // 输出 "0"
fmt.Println(5.0 / 9.0 * (f - 32)) // 输出 "100"
- 第一行:
(f - 32)
是 float64,乘以 5 后依然是 float64,然后除以 9,所有运算都是浮点运算,所以结果正确(100)。 - 第二行:
5 / 9
两个都是无类型整数(隐式为 int),整数除法结果为 0(因为 5/9 小于 1,用整数运算取整),导致整个表达式结果为 0。 - 第三行:
5.0
和9.0
是无类型浮点常量,所以 5.0/9.0 得到正确的浮点值,再乘以 (f - 32) 得到 100。
关键点:常量的写法决定了它们在运算中的默认类型,从而影响最终结果。这也说明了在使用无类型常量时,要注意数值字面量的形式(整数形式或浮点形式)对运算结果的影响。
fmt.Println(YiB / ZiB) // 输出 "1024"
- 即便 ZiB 和 YiB 的值超出了 Go 内置整数类型能表达的范围,它们仍然可以参与运算.
类型与接口转换
当无类型常量赋值给接口变量时,接口的动态类型取决于常量的默认类型
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)
这对接口编程非常关键,因为接口在运行时需要知道底层数据的具体类型。
语言特性
练习
-
编写KB、MB的常量声明,然后扩展到YB。
const (KB = 1000MB = 1000 * KBGB = 1000 * MBTB = 1000 * GBPB = 1000 * TBEB = 1000 * PBZB = 1000 * EBYB = 1000 * ZB )
总结
- iota 常量生成器:iota 是一个强大的工具,可以方便地生成有规律的数值序列,适用于枚举、位标志、以及其他有序数值集合的定义。
- 有一类常量称为“无类型常量”,这类常量在声明时不被赋予一个具体的基础类型,而是保持一种更“通用”的状态。这样做有两个好处:
- 更高精度的运算:无类型常量在算术运算中拥有比具体类型(如 int 或 float64)更高的精度(可以看作有 256 位或更多位的精度)。
- 灵活的隐式转换:当无类型常量赋值给变量时,编译器会自动将它们转换成变量所需要的类型(如果转换合法),这样可以直接用于多种场合,减少显式转换的麻烦。