Golang iota, not enums

Several common uses of iota

分享
Golang iota, not enums
generated by dalle-3

go iota, not enums

Note: Non-members can read the full story in this link.

Introduction

Last week, there was an article on Hacker News discussing Go Enums titled “Go Enums Suck” (I’m glad to see more information about Go on Hacker News). I don’t want to express any opinions on this article. The author’s points are all correct. If we consider iota as an enum, it is indeed quite bad compared to other languages. In this article, we will discuss the usage of iota.

Golang does not have a built-in keyword for enumerations. Generally, we use const + iota to achieve the ability to define enumerations. Someone may ask, why do we need to use enumerations? There is a highly upvoted answer on Stack Overflow that explains it:

“You should always use enums when a variable (especially a method parameter) can only take one out of a small set of possible values. Examples would be things like type constants (contract status: “permanent”, “temp”, “apprentice”), or flags (“execute now”, “defer execution”).
If you use enums instead of integers (or String codes), you increase compile-time checking and avoid errors from passing in invalid constants, and you document which values are legal to use.
BTW, overuse of enums might mean that your methods do too much (it’s often better to have several separate methods, rather than one method that takes several flags which modify what it does), but if you have to use flags or type codes, enums are the way to go.”

How to Implement Enum

iota is a pre-declared special constant in Go. It is pre-declared as 0, but its value is not fixed during compilation. When the pre-declared iota appears in a constant declaration, its value in the n-th constant description is n (starting from 0). Therefore, it only makes sense when there are multiple constant declarations of the same type.

For example, in an e-commerce system, the order status is essential. In such cases, we can typically do the following:

package main 
 ​ 
 import "fmt" 
 ​ 
 type OrderStatus int 
 ​ 
 const ( 
   Cancelled OrderStatus = iota // Order cancelled 0 
   NoPay                        // Not paid 1 
   Pending                      // Not shipped 2 
   Delivered                    // Delivered 3 
   Received                     // Received 4 
 ) 
 ​ 
 func main() { 
   fmt.Println(Cancelled, NoPay) // Prints: 0, 1 
 }

Of course, this may look cumbersome. Other constants can repeat the previous iota expression. We can simplify it like this:

package main 
 ​ 
 import "fmt" 
 ​ 
 type OrderStatus int 
 ​ 
 const ( 
   Cancelled OrderStatus = iota // Order cancelled 0 
   NoPay                        // Not paid 1 
   Pending                      // Not shipped 2 
   Delivered                    // Delivered 3 
   Received                     // Received 4 
 ) 
 ​ 
 func main() { 
   fmt.Println(Cancelled, NoPay) // Prints: 0, 1 
 }

Does anyone use the value 0 to represent a status? Generally not. We want to start with 1, so we can do it like this:

package main 
 ​ 
 import "fmt" 
 ​ 
 type OrderStatus int 
 ​ 
 const ( 
   Cancelled OrderStatus = iota + 1 // Order cancelled 1 
   NoPay                            // Not paid 2 
   Pending                          // Not shipped 3 
   Delivered                        // Delivered 4 
   Received                         // Received 5 
 ) 
 ​ 
 func main() { 
   fmt.Println(Cancelled, NoPay) // Prints: 1, 2 
 }

If we want to skip a number after Delivered and assign it to Received, which means Received = 6, we can use the _ symbol.

package main 
 ​ 
 import "fmt" 
 ​ 
 type OrderStatus int 
 ​ 
 const ( 
   Cancelled OrderStatus = iota + 1 // Order cancelled 1 
   NoPay                            // Not paid 2 
   Pending                          // Not shipped 3 
   Delivered                        // Delivered 4 
   _ 
   Received                         // Received 6 
 ) 
 ​ 
 func main() { 
   fmt.Println(Received) // Prints: 6 
 }

We can also go in reverse order.

package main 
 ​ 
 import "fmt" 
 ​ 
 type OrderStatus int 
 ​ 
 const ( 
   Max = 5 
 ) 
 ​ 
 const ( 
   Received  OrderStatus = Max - iota // Received 5 
   Delivered                          // Delivered 4 
   Pending                            // Not shipped 3 
   NoPay                              // Not paid 2 
   Cancelled                          // Order cancelled 1 
 ) 
 ​ 
 func main() { 
   fmt.Println(Received, Delivered) // Prints: 5, 4 
 }

We can even use bitwise operations, such as the following code snippet from the sync package in the Go source code, which deals with locks:

const ( 
   mutexLocked = 1 << iota  // 1<<0 
   mutexWoken               // 1<<1 
   mutexStarving            // 1<<2 
   mutexWaiterShift = iota  // 3 
 ) 
 ​ 
 func main() { 
   fmt.Println("Value of mutexLocked:", mutexLocked)   // Prints: 1 
   fmt.Println("Value of mutexWoken:", mutexWoken)     // Prints: 2 
   fmt.Println("Value of mutexStarving:", mutexStarving) // Prints: 4 
   fmt.Println("Value of mutexWaiterShift:", mutexWaiterShift) // Prints: 3 
 }

Some people may directly define constant values or use strings. For example, I have seen cases where string values are used to represent order statuses.

package main 
 ​ 
 import "fmt" 
 ​ 
 const ( 
   Cancelled = "cancelled" 
   NoPay     = "noPay" 
   Pending   = "pending" 
   Delivered = "delivered" 
   Received  = "received" 
 ) 
 ​ 
 var OrderStatusMsg = map[string]string{ 
   Cancelled: "Order cancelled", 
   NoPay:     "Not paid", 
   Pending:   "Not shipped", 
   Delivered: "Delivered", 
   Received:  "Received", 
 } 
 ​ 
 func main() { 
   fmt.Println(OrderStatusMsg[Cancelled]) 
 }

Or directly define integer constant values.

package main 
 
import "fmt" 
 
const ( 
 Cancelled = 1 
 NoPay     = 2 
 Pending   = 3 
 Delivered = 4 
 Received  = 5 
) 
 
var OrderStatusMsg = map[int]string{ 
 Cancelled: "Order cancelled", 
 NoPay:     "Not paid", 
 Pending:   "Not shipped", 
 Delivered: "Delivered", 
 Received:  "Received", 
} 
 
func main() { 
 fmt.Println(OrderStatusMsg[Cancelled]) 
}

Both approaches are valid, but using iota has advantages over the others:

  • Ensures the uniqueness of a set of constants, which manually-defined constants cannot guarantee.
  • Allows a group of actions to share the same behavior.
  • Avoids invalid values.
  • Improves code readability and maintainability.

Extension

Based on the demonstrations above, we can further enhance the implementation:

package main 
 
import ( 
 "fmt" 
) 
 
type OrderStatus int 
 
const ( 
 Cancelled OrderStatus = iota + 1 // Order cancelled 1 
 NoPay                            // Not paid 2 
 Pending                          // Not shipped 3 
 Delivered                        // Delivered 4 
 Received                         // Received 5 
) 
 
// Define a common behavior by assigning the type a String() function for easy printing of value meanings 
func (order OrderStatus) String() string { 
 return [...]string{"cancelled", "noPay", "pending", "delivered", "received"}[order-1] 
} 
 
// Create a common behavior by assigning the type an EnumIndex() function of type int 
func (order OrderStatus) EnumIndex() int { 
 return int(order) 
} 
 
func main() { 
 var order OrderStatus = Received 
 fmt.Println(order.String())    // Prints: received 
 fmt.Println(order.EnumIndex()) // Prints: 5 
}

Conclusion

This article mainly introduces the usage of iota in Go and explains why we should use it. The design of any programming language is always flawed, and we need to adapt to it.