Goenums:Go语言的类型安全枚举生成器
Goenums: Type Safe Enum Generator for Go

原始链接: https://github.com/zarldev/goenums

goenums 是一款工具,用于解决 Go 语言原生缺乏枚举类型的问题。它通过简单的基于 iota 的常量生成类型安全、功能丰富的枚举实现。它提供字符串转换、JSON 支持、数据库集成(SQL Scanner/Valuer)、验证以及 Go 1.21+ 的迭代功能。 主要功能包括:通过包装类型实现类型安全;自动字符串解析和表示(支持自定义名称);JSON 编解码;SQL 集成;以及确保处理所有枚举值的函数。可以通过注释向枚举添加自定义字段。使用 `-i` 标志启用不区分大小写的解析。快速失败模式 (`-f`) 在枚举无效时停止生成。传统模式 (`-l`) 支持较旧的 Go 版本。`-o` 标志指定输出格式。它与 `go:generate` 一起使用,用于自动代码重新生成。它通过引入强大的、可靠的且无需依赖的枚举功能,增强了 Go 语言的开发流程。

这篇 Hacker News 的帖子讨论了 Go 语言中缺少枚举类型以及背后的原因。讨论源于“Goenums:Go 语言的类型安全枚举生成器”的发布公告。 许多评论者对 Go 语言缺乏枚举类型表示惊讶,并将之与 Rust 和 Swift 等语言形成对比,在这些语言中,枚举类型是强大而直观的和类型。其他评论深入探讨了枚举类型在各种语言中的实现方式和理解方式的不同,有些是简单的整数别名,而另一些则是带标签的联合和类型。 一些评论者讨论了 Go 语言的设计理念,引用 Rob Pike 的理由:Go 语言的设计目标是易于普通程序员理解,这可能意味着牺牲了像枚举类型这样更复杂的特性。批评者认为这种简洁性是以牺牲安全性和表达能力为代价的,质疑 Go 语言与 Rust、C++ 和 Python 等语言相比的独特优势。一些人则为 Go 语言辩护,强调其易用性、快速的编译速度以及其适用于 Kubernetes 和 CLI 实用程序等云原生工具。

原文

License: MIT build

goenums addresses Go's lack of native enum support by generating comprehensive, type-safe enum implementations from simple constant declarations. Transform basic iota based constants into feature-rich enums with string conversion, validation, JSON handling, database integration, and more.

go install github.com/zarldev/goenums@latest

Documentation is available at https://zarldev.github.io/goenums.

  • Type Safety: Wrapper types prevent accidental misuse of enum values
  • String Conversion: Automatic string representation and parsing
  • JSON Support: Built-in marshaling and unmarshaling
  • Database Integration: SQL Scanner and Valuer implementations
  • Validation: Methods to check for valid enum values
  • Iteration: Modern Go 1.21+ iteration support with legacy fallback
  • Extensibility: Add custom fields to enums via comments
  • Exhaustive Handling: Helper functions to ensure you handle all enum values
  • Zero Dependencies: Completely dependency-free, using only the Go standard library
$ goenums -h
   ____ _____  ___  ____  __  ______ ___  _____
  / __ '/ __ \/ _ \/ __ \/ / / / __ '__ \/ ___/
 / /_/ / /_/ /  __/ / / / /_/ / / / / / (__  ) 
 \__, /\____/\___/_/ /_/\__,_/_/ /_/ /_/____/  
/____/
Usage: goenums [options] filename
Options:
  -help, -h
        Print help information
  -version, -v
        Print version information
  -failfast, -f
        Enable failfast mode - fail on generation of invalid enum while parsing (default: false)
  -legacy, -l
        Generate legacy code without Go 1.23+ iterator support (default: false)
  -insensitive, -i
        Generate case insensitive string parsing (default: false)
  -verbose, -vv
        Enable verbose mode - prints out the generated code (default: false)
  -output, -o string
        Specify the output format (default: go)

Custom String Representations

Handle custom string representations defined in the comments of each enum. Support for enum strings with spaces is supported by adding the alternative name in double quotes:

When the Alternative name does not contain spaces there is no need to add the double quotes.

type ticketStatus int

//go:generate goenums status.go
const (
    unknown   ticketStatus = iota // invalid Unknown
    pending                       // Pending
    approved                      // Approved
    rejected                      // Rejected
    completed                     // Completed
)

When using Alternative names that contain spaces, the double quotes are required.

type ticketStatus int

//go:generate goenums status.go
const (
    unknown   ticketStatus = iota // invalid "Not Found"
    pending                       // "In Progress"
    approved                      // "Fully Approved"
    rejected                      // "Has Been Rejected"
    completed                     // "Successfully Completed"
)

Extended Enum Types with Custom Fields

Add custom fields to your enums with type comments:

// Define fields in the type comment using one of three formats:
// 1. Space-separated: "Field Type,AnotherField Type"
// 2. Brackets: "Field[Type],AnotherField[Type]"
// 3. Parentheses: "Field(Type),AnotherField(Type)"

type planet int // Gravity float64,RadiusKm float64,MassKg float64,OrbitKm float64

//go:generate goenums planets.go
const (
    unknown planet = iota // invalid
    mercury               // Mercury 0.378,2439.7,3.3e23,57910000
    venus                 // Venus 0.907,6051.8,4.87e24,108200000
    earth                 // Earth 1,6378.1,5.97e24,149600000
	... 
)

Then we can use the extended enum type:

earthWeight := 100.0
fmt.Printf("Weight on %s: %.2f kg\n", 
    solarsystem.Planets.MARS, 
    earthWeight * solarsystem.Planets.MARS.Gravity)

Case Insensitive String Parsing

Use the -i flag to enable case insensitive string parsing:

//go:generate goenums -i status.go

// Generated code will parse case insensitive strings. All
// of the below will validate and produce the 'Pending' enum
status, err := validation.ParseStatus("Pending")
if err != nil {
    fmt.Println("error:", err)
}
status, err := validation.ParseStatus("pending")
if err != nil {
    fmt.Println("error:", err)
}
status, err := validation.ParseStatus("PENDING")
if err != nil {
    fmt.Println("error:", err)
}

The generated enum type also implements the json.Unmarshal and json.Marshal interfaces along with the sql.Scanner and sql.Valuer interfaces to handle parsing over the wire via HTTP or via a Database.

func (p Status) MarshalJSON() ([]byte, error) {
	return []byte(`"` + p.String() + `"`), nil
}

func (p *Status) UnmarshalJSON(b []byte) error {
	b = bytes.Trim(bytes.Trim(b, `"`), ` `)
	newp, err := ParseStatus(b)
	if err != nil {
		return err
	}
	*p = newp
	return nil
}

func (p *Status) Scan(value any) error {
	newp, err := ParseStatus(value)
	if err != nil {
		return err
	}
	*p = newp
	return nil
}

func (p Status) Value() (driver.Value, error) {
	return p.String(), nil
}

Ensure you handle all enum values with the generated Exhaustive function:

// Process all enum values safely
// This is especially useful in tests to ensure all enum values are covered
validation.ExhaustiveStatuses(func(status validation.Status) {
    // Process each status exactly once
    switch status {
    case validation.Statuses.FAILED:
        handleFailed()
    case validation.Statuses.PASSED:
        handlePassed()
    // ... handle all other cases
    }
})

// We can also iterate over all enum values to do exhaustive calculations
weightKg := 100.0
solarsystem.ExhaustivePlanets(func(p solarsystem.Planet) {
	// calculate weight on each planet
	gravity := p.Gravity
	planetMass := weightKg * gravity
	fmt.Printf("Weight on %s is %fKg with gravity %f\n", p, planetMass, gravity)
})

Iterator Support (Go 1.21+)

By default, goenums generates modern iterator support using Go 1.23's range-over-func feature:

// Using Go 1.21+ iterator
for status := range validation.Statuses.All() {
    fmt.Printf("Status: %s\n", status)
}
// There is the fallback method for slice based access
for _, status := range validation.Statuses.AllSlice() {
    fmt.Printf("Status: %s\n", status)
}

When using the legacy mode, the function is still called All() but it returns a slice of the enums.

// Legacy mode (or with -l flag)
for _, status := range validation.Statuses.All() {
    fmt.Printf("Status: %s\n", status)
}

Failfast Mode / Strict Mode

You can enable failfast mode by using the -failfast flag. This will cause the generator to fail on the first invalid enum it encounters while parsing.

//go:generate goenums -f status.go

// Generated code will return errors for invalid values
status, err := validation.ParseStatus("INVALID_STATUS")
if err != nil {
    fmt.Println("error:", err)
}

You can enable legacy mode by using the -legacy flag. This will generate code that is compatible with Go versions before 1.23.

You can enable verbose mode by using the -verbose flag. This will print out the generated code to the console.

You can specify the output format by using the -output flag. The default is go.

goenums is designed to work seamlessly with Go's standard tooling, particularly with go:generate directives. This allows you to automatically regenerate your enum code whenever your source files change, integrating smoothly into your existing build process.

  1. Define your enum constant in a Go file:
package validation

type status int

//go:generate goenums status.go
const (
	unknown status = iota // invalid
	failed
	passed
	skipped
	scheduled
	running
	booked
)
  1. Run go generate ./... to generate the enum implementations.

  2. Use the generated status enums type in your code:

// Access enum constants safely
myStatus := validation.Statuses.PASSED

// Convert to string
fmt.Println(myStatus.String()) // "PASSED"

// Parse from various sources
input := "SKIPPED"
parsed, _ := validation.ParseStatus(input)

// Validate enum values
if !parsed.IsValid() {
    fmt.Println("Invalid status")
}

// JSON marshaling/unmarshaling works automatically
type Task struct {
    ID     int              `json:"id"`
    Status validation.Status `json:"status"`
}
  • Go 1.21+ for iterator support (or use -l flag for legacy mode)

Input source go file:

package solarsystem

type planet int // Gravity float64,RadiusKm float64,MassKg float64,OrbitKm float64,OrbitDays float64,SurfacePressureBars float64,Moons int,Rings bool

//go:generate goenums planets.go
const (
	unknown planet = iota // invalid
	mercury               // Mercury 0.378,2439.7,3.3e23,57910000,88,0.0000000001,0,false
	venus                 // Venus 0.907,6051.8,4.87e24,108200000,225,92,0,false
	earth                 // Earth 1,6378.1,5.97e24,149600000,365,1,1,false
	mars                  // Mars 0.377,3389.5,6.42e23,227900000,687,0.01,2,false
	jupiter               // Jupiter 2.36,69911,1.90e27,778600000,4333,20,4,true
	saturn                // Saturn 0.916,58232,5.68e26,1433500000,10759,1,7,true
	uranus                // Uranus 0.889,25362,8.68e25,2872500000,30687,1.3,13,true
	neptune               // Neptune 1.12,24622,1.02e26,4495100000,60190,1.5,2,true
)

Produces a go output file called planets_enums.go with the following content:

// DO NOT EDIT.
// Code generated by goenums 'v0.3.6' at 2025-04-19 03:14:04.
// github.com/zarldev/goenums
//
// using the command:
//
// goenums planets.go


package solarsystem

import (
	"bytes"
	"database/sql/driver"
	"fmt"
	"iter"
	"strconv"
)

type Planet struct {
	planet
	Gravity             float64
	RadiusKm            float64
	MassKg              float64
	OrbitKm             float64
	OrbitDays           float64
	SurfacePressureBars float64
	Moons               int
	Rings               bool
}

type planetsContainer struct {
	UNKNOWN Planet
	MERCURY Planet
	VENUS   Planet
	EARTH   Planet
	MARS    Planet
	JUPITER Planet
	SATURN  Planet
	URANUS  Planet
	NEPTUNE Planet
}

var Planets = planetsContainer{
	MERCURY: Planet{
		planet:              mercury,
		Gravity:             0.378000,
		RadiusKm:            2439.700000,
		MassKg:              330000000000000029360128.000000,
		OrbitKm:             57910000.000000,
		OrbitDays:           88.000000,
		SurfacePressureBars: 0.000000,
		Moons:               0,
		Rings:               false,
	},
	VENUS: Planet{
		planet:              venus,
		Gravity:             0.907000,
		RadiusKm:            6051.800000,
		MassKg:              4869999999999999601541120.000000,
		OrbitKm:             108200000.000000,
		OrbitDays:           225.000000,
		SurfacePressureBars: 92.000000,
		Moons:               0,
		Rings:               false,
	},
	EARTH: Planet{
		planet:              earth,
		Gravity:             1.000000,
		RadiusKm:            6378.100000,
		MassKg:              5970000000000000281018368.000000,
		OrbitKm:             149600000.000000,
		OrbitDays:           365.000000,
		SurfacePressureBars: 1.000000,
		Moons:               1,
		Rings:               false,
	},
	MARS: Planet{
		planet:              mars,
		Gravity:             0.377000,
		RadiusKm:            3389.500000,
		MassKg:              642000000000000046137344.000000,
		OrbitKm:             227900000.000000,
		OrbitDays:           687.000000,
		SurfacePressureBars: 0.010000,
		Moons:               2,
		Rings:               false,
	},
	JUPITER: Planet{
		planet:              jupiter,
		Gravity:             2.360000,
		RadiusKm:            69911.000000,
		MassKg:              1900000000000000107709726720.000000,
		OrbitKm:             778600000.000000,
		OrbitDays:           4333.000000,
		SurfacePressureBars: 20.000000,
		Moons:               4,
		Rings:               true,
	},
	SATURN: Planet{
		planet:              saturn,
		Gravity:             0.916000,
		RadiusKm:            58232.000000,
		MassKg:              568000000000000011945377792.000000,
		OrbitKm:             1433500000.000000,
		OrbitDays:           10759.000000,
		SurfacePressureBars: 1.000000,
		Moons:               7,
		Rings:               true,
	},
	URANUS: Planet{
		planet:              uranus,
		Gravity:             0.889000,
		RadiusKm:            25362.000000,
		MassKg:              86800000000000000905969664.000000,
		OrbitKm:             2872500000.000000,
		OrbitDays:           30687.000000,
		SurfacePressureBars: 1.300000,
		Moons:               13,
		Rings:               true,
	},
	NEPTUNE: Planet{
		planet:              neptune,
		Gravity:             1.120000,
		RadiusKm:            24622.000000,
		MassKg:              102000000000000007952400384.000000,
		OrbitKm:             4495100000.000000,
		OrbitDays:           60190.000000,
		SurfacePressureBars: 1.500000,
		Moons:               2,
		Rings:               true,
	},
}

// invalidPlanet represents an invalid or undefined Planet value.
// It is used as a default return value for failed parsing or conversion operations.
var invalidPlanet = Planet{}

// allSlice is an internal method that returns all valid Planet values as a slice.
func (c planetsContainer) allSlice() []Planet {
	return []Planet{
		c.MERCURY,
		c.VENUS,
		c.EARTH,
		c.MARS,
		c.JUPITER,
		c.SATURN,
		c.URANUS,
		c.NEPTUNE,
	}
}

// AllSlice returns all valid Planet values as a slice.
// Deprecated: Use All() with Go 1.21+ range over function types instead.
func (c planetsContainer) AllSlice() []Planet {
	return c.allSlice()
}

// All returns all valid Planet values.
// In Go 1.21+, this can be used with range-over-function iteration:
// ```
//
//	for v := range Planets.All() {
//	    // process each enum value
//	}
//
// ```
func (c planetsContainer) All() iter.Seq[Planet] {
	return func(yield func(Planet) bool) {
		for _, v := range c.allSlice() {
			if !yield(v) {
				return
			}
		}
	}
}

// ParsePlanet converts various input types to a Planet value.
// It accepts the following types:
// - Planet: returns the value directly
// - string: parses the string representation
// - []byte: converts to string and parses
// - fmt.Stringer: uses the String() result for parsing
// - int/int32/int64: converts the integer to the corresponding enum value
//
// If the input cannot be converted to a valid Planet value, it returns
// the invalidPlanet value without an error.
func ParsePlanet(a any) (Planet, error) {
	res := invalidPlanet
	switch v := a.(type) {
	case Planet:
		return v, nil
	case []byte:
		res = stringToPlanet(string(v))
	case string:
		res = stringToPlanet(v)
	case fmt.Stringer:
		res = stringToPlanet(v.String())
	case int:
		res = intToPlanet(v)
	case int64:
		res = intToPlanet(int(v))
	case int32:
		res = intToPlanet(int(v))
	}
	return res, nil
}

// stringToPlanet is an internal function that converts a string to a Planet value.
// It uses a predefined mapping of string representations to enum values.
var (
	_planetsNameMap = map[string]Planet{
		"unknown": Planets.UNKNOWN, // Primary alias
		"Mercury": Planets.MERCURY, // Primary alias
		"mercury": Planets.MERCURY, // Enum name
		"Venus":   Planets.VENUS,   // Primary alias
		"venus":   Planets.VENUS,   // Enum name
		"Earth":   Planets.EARTH,   // Primary alias
		"earth":   Planets.EARTH,   // Enum name
		"Mars":    Planets.MARS,    // Primary alias
		"mars":    Planets.MARS,    // Enum name
		"Jupiter": Planets.JUPITER, // Primary alias
		"jupiter": Planets.JUPITER, // Enum name
		"Saturn":  Planets.SATURN,  // Primary alias
		"saturn":  Planets.SATURN,  // Enum name
		"Uranus":  Planets.URANUS,  // Primary alias
		"uranus":  Planets.URANUS,  // Enum name
		"Neptune": Planets.NEPTUNE, // Primary alias
		"neptune": Planets.NEPTUNE, // Enum name
	}
)

func stringToPlanet(s string) Planet {
	if v, ok := _planetsNameMap[s]; ok {
		return v
	}
	return invalidPlanet
}

// intToPlanet converts an integer to a Planet value.
// The integer is treated as the ordinal position in the enum sequence.
// If the integer doesn't correspond to a valid enum value, invalidPlanet is returned.
func intToPlanet(i int) Planet {
	if i < 0 || i >= len(Planets.allSlice()) {
		return invalidPlanet
	}
	return Planets.allSlice()[i]
}

// ExhaustivePlanets calls the provided function once for each valid Planets value.
// This is useful for switch statement exhaustiveness checking and for processing all enum values.
// Example usage:
// ```
//
//	ExhaustivePlanets(func(x Planet) {
//	    switch x {
//	    case Planets.Neptune:
//	        // handle Neptune
//	    }
//	})
//
// ```
func ExhaustivePlanets(f func(Planet)) {
	for _, p := range Planets.allSlice() {
		f(p)
	}
}

// validPlanets is a map of valid Planet values.
var validPlanets = map[Planet]bool{
	Planets.MERCURY: true,
	Planets.VENUS:   true,
	Planets.EARTH:   true,
	Planets.MARS:    true,
	Planets.JUPITER: true,
	Planets.SATURN:  true,
	Planets.URANUS:  true,
	Planets.NEPTUNE: true,
}

// IsValid checks whether the Planet value is valid.
// A valid value is one that is defined in the original enum and not marked as invalid.
func (p Planet) IsValid() bool {
	return validPlanets[p]
}

// MarshalJSON implements the json.Marshaler interface for Planet.
// The enum value is encoded as its string representation.
func (p Planet) MarshalJSON() ([]byte, error) {
	return []byte(`"` + p.String() + `"`), nil
}

// UnmarshalJSON implements the json.Unmarshaler interface for Planet.
// It supports unmarshaling from a string representation of the enum.
func (p *Planet) UnmarshalJSON(b []byte) error {
	b = bytes.Trim(bytes.Trim(b, `"`), ` `)
	newp, err := ParsePlanet(b)
	if err != nil {
		return err
	}
	*p = newp
	return nil
}

// Scan implements the sql.Scanner interface for Planet.
// This allows Planet values to be scanned directly from database queries.
// It supports scanning from strings, []byte, or integers.
func (p *Planet) Scan(value any) error {
	newp, err := ParsePlanet(value)
	if err != nil {
		return err
	}
	*p = newp
	return nil
}

// Value implements the driver.Valuer interface for Planet.
// This allows Planet values to be saved to databases.
// The value is stored as a string representation of the enum.
func (p Planet) Value() (driver.Value, error) {
	return p.String(), nil
}

func _() {
	// An "invalid array index" compiler error signifies that the constant values have changed.
	// Re-run the goenums command to generate them again.
	// Does not identify newly added constant values unless order changes
	var x [1]struct{}
	_ = x[unknown-0]
	_ = x[mercury-1]
	_ = x[venus-2]
	_ = x[earth-3]
	_ = x[mars-4]
	_ = x[jupiter-5]
	_ = x[saturn-6]
	_ = x[uranus-7]
	_ = x[neptune-8]
}

const _planets_name = "unknownMercuryVenusEarthMarsJupiterSaturnUranusNeptune"

var _planets_index = [...]uint16{0, 7, 14, 19, 24, 28, 35, 41, 47, 54}

// String returns the string representation of the Planet value.
// For valid values, it returns the name of the constant.
// For invalid values, it returns a string in the format "planets(N)",
// where N is the numeric value.
func (i planet) String() string {
	if i < 0 || i >= planet(len(_planets_index)-1) {
		return "planets(" + (strconv.FormatInt(int64(i), 10) + ")")
	}
	return _planets_name[_planets_index[i]:_planets_index[i+1]]
}

For more examples, see those used for testing in the testdata directory.

go-recipes

MIT License - See LICENSE for full details.

联系我们 contact @ memedata.com