大家好,我是极客老墨。

写 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

关键点iotaconst 块的第一行就开始计数,不管你用没用它。

同一行多个常量,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 的常量设计有三个亮点:

  1. 无类型常量:不指定类型,Go 自动适配,省掉类型转换的麻烦
  2. iota 自动递增:定义枚举只需写一次,后面自动 +1,比手写 0、1、2 优雅多了
  3. 自定义类型保证类型安全:用 type WeekDay int 限制参数类型,避免乱传值

实战建议

  • 常量默认别指定类型,让 Go 推导
  • 定义枚举用 iota,别手写递增
  • 需要类型安全时,自定义类型 + 有类型常量

Go 内置的常量除了 iota,还有 truefalsenil。记住这些,常量这块就没啥坑了。

老墨踩过的坑

坑 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)

如果数据库里存的是数字,这样改会导致数据错乱。

教训

  1. 新增枚举值加在最后,不要插在中间
  2. 或者显式指定值:RoleAdmin = 1RoleUser = 2
  3. 更好的做法:数据库存字符串,不存数字

坑 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)

要求:

  1. 定义 200、201、204、400、401、403、404、500
  2. 实现 String() 方法
  3. 编写测试用例

练习题 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}

思考题

  1. 为什么 Go 不提供 enum 关键字? iota 相比传统 enum 有什么优势和劣势?
  2. 无类型常量的设计理念是什么? 它解决了什么问题?
  3. 什么时候应该使用有类型常量? 什么时候用无类型常量?
  4. 位标志(Bit Flags)适合什么场景? 你能想到哪些实际应用?

评论区秀出你的想法,大家一起讨论!


你定义枚举时用过 iota 吗?有没有遇到过 iota 的奇怪行为?欢迎评论区讨论!

极客老墨,继续折腾!

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

完整示例代码在 go-tutorial-code/04-const


相关阅读