大家好,我是极客老墨。
写 Go 之前,我在 Java 里定义枚举都是老老实实写 enum。转到 Go 发现没有 enum 关键字,心想"这不得手写 0、1、2、3?"
结果发现 Go 给了个更骚的东西:iota。自动递增,省掉一堆重复代码。
更绝的是 Go 的"无类型常量"设计,直接让你不用管类型转换的破事儿。这篇就聊聊 Go 常量的几个巧妙设计。
常量基础:用 const 声明
常量就是编译期就确定值、运行时不能改的东西。声明很简单:
1const c1 = 1
2const c2 = -100
3const c3 = "hello"
跟变量一样,可以用括号批量声明:
1const (
2 c4 = 3.14
3 c5 = 1.2 + 12i // Go 支持复数,数学系的福音
4)
⚠️ 小坑:不赋值会复制上一个值
批量声明时,如果某个常量不赋值,它会复制上一个非空表达式的值:
1const (
2 m = 1 // 1
3 n // 1,复制 m 的值
4 k // 1,继续复制
5 l = m + 1 // 2,新表达式
6 i // 2,复制 l 的表达式结果
7)
8fmt.Println(m, n, k, l, i) // 输出:1 1 1 2 2
这个特性配合 iota 用起来很爽,后面会讲。
Go 的神设计:无类型常量
你注意到没?前面声明常量时都没写类型。这是 Go 的一个巧妙设计:无类型常量。
有类型常量的麻烦
Go 是强类型语言,类型不同就不能直接运算,哪怕底层类型一样:
1func add() {
2 var x int = 1
3 var y int32 = 2
4 // fmt.Println(x + y) // ❌ 编译失败:类型不匹配
5 fmt.Println(x + int(y)) // ✅ 必须强制转换
6}
7
8type MyInt int
9
10func add2() {
11 var x int = 1
12 var y MyInt = 2
13 // fmt.Println(x + y) // ❌ 编译失败
14 fmt.Println(MyInt(x) + y) // ✅ 即使底层都是 int,也得转
15}
这种严格性在大项目里是好事,但写小代码时确实烦。Go 的解决方案:无类型常量。
无类型常量:自动适配类型
声明常量时不指定类型,Go 会在使用时自动转换。看对比:
有类型常量(麻烦):
1const pi float32 = 3.1415926
2const r int = 4
3// const area = pi * r * r // ❌ 编译失败:类型不匹配
4const area = pi * float32(r) * float32(r) // 必须手动转换
5fmt.Println("r = ", r, ", area = ", area)
无类型常量(舒服):
1const pi = 3.1415926 // 不指定类型
2const r = 4
3const area = pi * r * r // ✅ 直接算,Go 自动处理类型
4fmt.Printf("r type: %T, pi type: %T, area type: %T\n", r, pi, area)
5fmt.Println("r = ", r, ", area = ", area)
输出:
r type: int, pi type: float64, area type: float64
r = 4 , area = 50.2654816
看到没?虽然 r 是 int,但跟 float64 的 pi 直接运算,编译器自动搞定类型转换。这就是无类型常量的魔法。
老墨建议:大部分情况下,常量别指定类型,让 Go 自己推导。除非你需要类型安全(后面会讲)。
iota:Go 的枚举神器
Go 没有 enum 关键字,但给了个更灵活的东西:iota。
传统方式:手写递增(累)
1const (
2 Monday = 0
3 Tuesday = 1
4 Wednesday = 2
5 Thursday = 3
6 Friday = 4
7 Saturday = 5
8 Sunday = 6
9 weekDays = 7
10)
看着就累,0 到 7 手写一遍。
用 iota:自动递增(爽)
iota 是个魔法数字,规则:
- 每次
const出现时重置为 0 - 每声明一个常量,自动 +1
- 从
const后第一个非空表达式开始计数
看例子:
1const (
2 x = -1 // -1,iota = 0(虽然没用 iota,但已经开始计数)
3 a = 1 // 1,iota = 1
4 b // 1(复制上一个值),iota = 2
5 c = iota // 3(终于用上了),iota = 3
6 d // 4(复制 c = iota),iota = 4
7)
8fmt.Println(x, a, b, c, d) // 输出:-1 1 1 3 4
9
10// 新的 const 块,iota 重置
11const c1 = iota // 0
12const c2 = iota // 0(又是新的 const 块)
13fmt.Println(c1, c2) // 输出:0 0
关键点:iota 从 const 块的第一行就开始计数,不管你用没用它。
同一行多个常量,iota 值相同
1const (
2 e, f = iota, iota + 10 // 0, 10(同一行,iota 都是 0)
3 g, h // 1, 11(复制表达式,iota 变成 1)
4)
5fmt.Println(e, f, g, h) // 输出:0 10 1 11
改写星期枚举
1const (
2 Monday = iota // 0
3 Tuesday // 1
4 Wednesday // 2
5 Thursday // 3
6 Friday // 4
7 Saturday // 5
8 Sunday // 6
9 weekDays // 7
10)
只写一次 iota,后面自动递增。省了一堆重复代码,这就是 Go 的枚举方式。
进阶:类型安全的枚举
前面的枚举有个问题:任何 int 都能传进来。
1func checkWorkday(day int) {
2 if day == Saturday || day == Sunday {
3 fmt.Println("(〃'▽'〃) 周末快乐!")
4 } else {
5 fmt.Println("(灬ꈍ ꈍ灬) 又是搬砖的一天...")
6 }
7}
8
9// 调用时可以传任何 int
10checkWorkday(999) // 编译通过,但 999 不是合法的星期
这不行,得限制参数类型。解决方案:自定义类型。
1type WeekDay int // 自定义类型
2
3const (
4 Monday WeekDay = iota // 指定类型为 WeekDay
5 Tuesday
6 Wednesday
7 Thursday
8 Friday
9 Saturday
10 Sunday
11 weekDays
12)
现在常量都是 WeekDay 类型,函数参数也改成这个类型:
1func checkWorkday(day WeekDay) { // 参数类型限定为 WeekDay
2 if day == Saturday || day == Sunday {
3 fmt.Println("(〃'▽'〃) 周末快乐!")
4 } else {
5 fmt.Println("(灬ꈍ ꈍ灬) 又是搬砖的一天...")
6 }
7}
8
9// 调用
10checkWorkday(Monday) // ✅ 正确
11// checkWorkday(999) // ❌ 编译失败:类型不匹配
这样就有了一定的类型安全。虽然还是能强转 checkWorkday(WeekDay(999)),但至少调用者知道该传什么类型了。
老墨总结
Go 的常量设计有三个亮点:
- 无类型常量:不指定类型,Go 自动适配,省掉类型转换的麻烦
- iota 自动递增:定义枚举只需写一次,后面自动 +1,比手写 0、1、2 优雅多了
- 自定义类型保证类型安全:用
type WeekDay int限制参数类型,避免乱传值
实战建议:
- 常量默认别指定类型,让 Go 推导
- 定义枚举用
iota,别手写递增 - 需要类型安全时,自定义类型 + 有类型常量
Go 内置的常量除了 iota,还有 true、false、nil。记住这些,常量这块就没啥坑了。
老墨踩过的坑
坑 1:iota 跳过值导致的 bug
老墨刚开始用 iota 时,写过这样的代码:
1const (
2 StatusPending = iota // 0
3 StatusRunning // 1
4 // StatusPaused = 2 // 注释掉了,以为不会影响后面
5 StatusSuccess // 2(不是 3!)
6 StatusFailed // 3
7)
后来数据库里存的 StatusPaused = 2,结果代码里 2 变成了 StatusSuccess,导致状态判断全乱了。
教训:如果要跳过某个值,必须显式占位:
1const (
2 StatusPending = iota // 0
3 StatusRunning // 1
4 _ // 2,占位符
5 StatusSuccess // 3
6 StatusFailed // 4
7)
坑 2:const 块中间插入新常量
1// 原来的代码
2const (
3 RoleAdmin = iota // 0
4 RoleUser // 1
5)
6
7// 后来加了个 RoleModerator
8const (
9 RoleAdmin = iota // 0
10 RoleModerator // 1(新加的)
11 RoleUser // 2(值变了!)
12)
如果数据库里存的是数字,这样改会导致数据错乱。
教训:
- 新增枚举值加在最后,不要插在中间
- 或者显式指定值:
RoleAdmin = 1,RoleUser = 2 - 更好的做法:数据库存字符串,不存数字
坑 3:忘记 iota 从 0 开始
1const (
2 January = iota // 0(不是 1!)
3 February // 1
4 March // 2
5 // ...
6)
月份从 1 开始,但 iota 从 0 开始。解决方案:
1const (
2 _ = iota // 0,占位
3 January // 1
4 February // 2
5 // ...
6)
7
8// 或者
9const (
10 January = iota + 1 // 1
11 February // 2
12 March // 3
13)
实战建议
1. 枚举命名规范
1// ✅ 好的命名:类型名 + 值名
2type OrderStatus int
3const (
4 OrderStatusPending OrderStatus = iota
5 OrderStatusPaid
6 OrderStatusShipped
7 OrderStatusDelivered
8)
9
10// ❌ 不好的命名:容易冲突
11const (
12 Pending = iota // 太通用,容易和其他包冲突
13 Paid
14 Shipped
15)
2. 为枚举添加 String() 方法
1type WeekDay int
2
3const (
4 Monday WeekDay = iota
5 Tuesday
6 Wednesday
7 Thursday
8 Friday
9 Saturday
10 Sunday
11)
12
13// 实现 String() 方法,方便调试和日志
14func (w WeekDay) String() string {
15 names := [...]string{
16 "Monday", "Tuesday", "Wednesday", "Thursday",
17 "Friday", "Saturday", "Sunday",
18 }
19 if w < Monday || w > Sunday {
20 return "Unknown"
21 }
22 return names[w]
23}
24
25// 使用
26fmt.Println(Monday) // 输出:Monday(而不是 0)
3. 位标志(Bit Flags)的妙用
iota 配合位运算,可以实现多选枚举:
1type Permission uint
2
3const (
4 PermRead Permission = 1 << iota // 1 (二进制: 001)
5 PermWrite // 2 (二进制: 010)
6 PermExecute // 4 (二进制: 100)
7)
8
9// 组合权限
10const (
11 PermReadWrite = PermRead | PermWrite // 3 (011)
12 PermAll = PermRead | PermWrite | PermExecute // 7 (111)
13)
14
15// 检查权限
16func hasPermission(userPerm, checkPerm Permission) bool {
17 return userPerm&checkPerm == checkPerm
18}
19
20// 使用
21userPerm := PermRead | PermWrite
22fmt.Println(hasPermission(userPerm, PermRead)) // true
23fmt.Println(hasPermission(userPerm, PermExecute)) // false
4. 常量表达式的计算
常量可以用表达式,编译期就会计算:
1const (
2 KB = 1024
3 MB = 1024 * KB
4 GB = 1024 * MB
5 TB = 1024 * GB
6)
7
8const (
9 SecondsPerMinute = 60
10 SecondsPerHour = 60 * SecondsPerMinute
11 SecondsPerDay = 24 * SecondsPerHour
12)
练习题
练习题 1:定义 HTTP 状态码(⭐)
使用 iota 定义常见的 HTTP 状态码:
1type HTTPStatus int
2
3const (
4 StatusOK HTTPStatus = iota + 200 // 200
5 StatusCreated // 201
6 // 补充其他状态码
7)
要求:
- 定义 200、201、204、400、401、403、404、500
- 实现
String()方法 - 编写测试用例
练习题 2:实现文件权限(⭐⭐)
模仿 Unix 文件权限,使用位标志实现:
1type FileMode uint
2
3const (
4 ModeRead FileMode = 1 << iota // 读权限
5 ModeWrite // 写权限
6 ModeExecute // 执行权限
7)
8
9// 实现以下方法
10func (m FileMode) CanRead() bool { /* ... */ }
11func (m FileMode) CanWrite() bool { /* ... */ }
12func (m FileMode) CanExecute() bool { /* ... */ }
练习题 3:枚举类型安全(⭐⭐⭐)
实现一个类型安全的颜色枚举:
1type Color int
2
3const (
4 Red Color = iota
5 Green
6 Blue
7)
8
9// 要求:
10// 1. 实现 String() 方法
11// 2. 实现 IsValid() 方法,检查是否是合法的颜色
12// 3. 实现 FromString(s string) (Color, error) 方法
13// 4. 编写完整的测试用例
练习题 4:iota 跳跃(⭐⭐)
理解以下代码的输出:
1const (
2 a = iota * 2 // 0
3 b // 2
4 c // 4
5 d = iota // 3(为什么?)
6 e // 4
7)
8fmt.Println(a, b, c, d, e)
解释为什么 d 是 3 而不是 6?
练习题 5:实战应用(⭐⭐⭐)
设计一个订单状态机:
1type OrderStatus int
2
3const (
4 OrderCreated OrderStatus = iota
5 OrderPaid
6 OrderShipped
7 OrderDelivered
8 OrderCancelled
9 OrderRefunded
10)
11
12// 实现状态转换规则
13func (s OrderStatus) CanTransitionTo(target OrderStatus) bool {
14 // 实现状态转换逻辑
15 // 例如:已取消的订单不能转为已支付
16}
思考题
- 为什么 Go 不提供 enum 关键字? iota 相比传统 enum 有什么优势和劣势?
- 无类型常量的设计理念是什么? 它解决了什么问题?
- 什么时候应该使用有类型常量? 什么时候用无类型常量?
- 位标志(Bit Flags)适合什么场景? 你能想到哪些实际应用?
评论区秀出你的想法,大家一起讨论!
你定义枚举时用过 iota 吗?有没有遇到过 iota 的奇怪行为?欢迎评论区讨论!
极客老墨,继续折腾!
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
完整示例代码在 go-tutorial-code/04-const。