Question
I want to represent a simplified chromosome in Go. A chromosome consists of N bases, and each base can only be one of these values: A, C, T, or G.
I would like to enforce these allowed values using an enum-like type. Since Go does not have enums in the same way as some other languages, what is the most idiomatic way to model this in Go?
Short Answer
By the end of this page, you will understand how Go typically represents enum-like values using custom types and constants, usually with iota when sequential values are useful. You will also see how to model a fixed set of values such as DNA bases (A, C, T, G), how to print them nicely, how to validate them, and how this pattern is used in real Go codebases.
Concept
In Go, the idiomatic replacement for enums is usually:
- Define a custom named type
- Declare a fixed set of typed constants for that type
For example:
type Base int
const (
A Base = iota
C
T
G
)
This gives you a type named Base with a limited set of intended constant values.
Why Go uses this approach
Go keeps the language small and simple. Instead of adding a separate enum feature, it relies on:
- named types
- constants
- methods
- optional validation
This works well because you can still:
- restrict your API to a specific type
- attach methods like
String() - validate input when needed
- use
switchstatements clearly
Important limitation
Go's enum-like pattern does not prevent someone from creating an invalid value directly:
var b Base = 99
That is still valid Go syntax because Base is just a named type underneath. So the idiomatic approach is:
- use typed constants to define valid values
- expose constructors or parsing functions if validation matters
- add methods such as
Valid()orString()for safety and readability
For your chromosome example
A DNA base is a perfect use case for enum-like values because:
- there is a small fixed set of valid options
- the values have clear meaning
- code becomes easier to read with names like
AandC - functions can accept
Baseinstead of raw strings or integers
This is more expressive than passing around plain string or byte values everywhere.
Mental Model
Think of a Go enum as a custom label system rather than a locked box.
- The type is the category, like
Base - The constants are the approved labels, like
A,C,T, andG - Your code agrees to use only those labels
It is similar to creating a set of official badges for a building:
A,C,T,Gare the official badges- the badge type is
Base - someone could theoretically forge a fake badge, so you may still need validation at the entrance
That is how Go enums work: they communicate intent clearly, but validation is still your responsibility when values come from outside your program.
Syntax and Examples
Basic enum-like pattern in Go
package main
import "fmt"
type Base int
const (
A Base = iota
C
T
G
)
func main() {
var b Base = A
fmt.Println(b)
}
This compiles, but printing b will show its numeric value unless you define a String() method.
Adding a String method
package main
import "fmt"
type Base int
const (
A Base = iota
C
T
G
)
func (b Base) String() string {
switch b {
case A:
return "A"
case C:
return "C"
case T:
return "T"
case G:
return "G"
default:
return "Unknown"
}
}
func main() {
chromosome := []Base{A, C, T, G, A}
for _, base := range chromosome {
fmt.Print(base, " ")
}
}
Output:
A C T G A
Parsing external input safely
If values come from a file, user input, or API, validation matters.
package main
import (
"fmt"
)
type Base int
const (
A Base = iota
C
T
G
)
func (b Base) String() string {
switch b {
case A:
return "A"
case C:
return "C"
case T:
return "T"
case G:
return "G"
default:
return "Unknown"
}
}
func ParseBase(s string) (Base, error) {
switch s {
case "A":
return A, nil
case "C":
return C, nil
case "T":
return T, nil
case "G":
return G, nil
default:
return 0, fmt.Errorf("invalid base: %s", s)
}
}
This is a common real-world pattern in Go: define constants, then provide parsing and validation helpers.
Alternative: use characters directly
Since DNA bases are naturally single characters, another practical option is:
type Base byte
const (
A Base = 'A'
C Base = 'C'
T Base = 'T'
G Base = 'G'
)
This can be very readable because the underlying value already matches the domain concept.
Step by Step Execution
Consider this example:
package main
import "fmt"
type Base byte
const (
A Base = 'A'
C Base = 'C'
T Base = 'T'
G Base = 'G'
)
func main() {
chromosome := []Base{A, G, C}
for _, base := range chromosome {
fmt.Printf("%c\n", base)
}
}
Here is what happens step by step:
-
type Base byte- Creates a new named type called
Base - Its underlying type is
byte
- Creates a new named type called
-
The constants are declared:
Ahas value'A'Chas value'C'Thas value'T'Ghas value'G'
-
chromosome := []Base{A, G, C}- Creates a slice containing three DNA bases
-
The
forloop iterates through each element- First iteration:
baseisA - Second iteration:
baseisG - Third iteration:
baseisC
- First iteration:
-
fmt.Printf("%c\n", base)%cprints the byte as a character- Output becomes:
A
G
C
Why this version is nice
Using byte with character literals works especially well when:
- the values naturally map to characters
- you want compact storage
- you often convert to or from strings
For this specific DNA example, type Base byte is often more practical than type Base int.
Real World Use Cases
Enum-like types in Go are commonly used anywhere a value must come from a small fixed set.
Common examples
- Status values:
Pending,Running,Failed,Completed - HTTP or job states:
Queued,Processing,Done - User roles:
Admin,Editor,Viewer - Configuration modes:
Debug,Test,Production - Domain-specific symbols: DNA bases like
A,C,T,G
In your chromosome case
You might use a Base type for:
- storing a chromosome as
[]Base - validating incoming DNA data
- counting how many times each base appears
- comparing sequences
- converting between text input and internal values
Example:
func CountBase(sequence []Base, target Base) int {
count := 0
for _, b := range sequence {
if b == target {
count++
}
}
return count
}
This is clearer than using raw strings everywhere because target is explicitly a Base.
Real Codebase Usage
In real Go projects, enum-like values are usually wrapped with a few supporting patterns.
1. Typed constants for domain meaning
type Status int
const (
Pending Status = iota
Running
Failed
Done
)
This makes function signatures clearer:
func SetStatus(s Status) {}
2. String methods for logging and debugging
Developers often implement String() so logs and printed output are readable.
func (s Status) String() string {
switch s {
case Pending:
return "pending"
case Running:
return "running"
case Failed:
return "failed"
case Done:
return "done"
default:
return "unknown"
}
}
3. Parsing and validation for external input
When values come from JSON, forms, environment variables, or APIs, developers validate them explicitly.
Patterns include:
ParseStatus(string) (Status, error)Valid() bool- custom JSON marshal/unmarshal methods when necessary
4. Guard clauses
A common pattern is rejecting invalid enum values early.
func ProcessBase(b Base) error {
switch b {
case A, C, T, G:
return nil
default:
return fmt.Errorf("invalid base")
}
}
5. Choosing the underlying type carefully
Developers pick the underlying type based on usage:
intfor general enum-like valuesstringwhen readability matters stronglybytewhen the domain is naturally character-based
For DNA bases, byte is often a very natural fit.
Common Mistakes
1. Using plain strings everywhere
This works, but it loses type safety.
func SaveBase(b string) {}
Now any string is accepted:
SaveBase("banana")
Better:
type Base byte
2. Assuming Go enums are fully restricted
Beginners often expect this to be impossible:
var b Base = 99
But it is allowed. Go enum-like types are not closed sets enforced by the compiler.
How to avoid problems
- validate external input
- expose constructor or parser functions
- use methods like
Valid()
Example:
func (b Base) Valid() bool {
switch b {
case A, C, T, G:
return true
default:
return false
}
}
3. Printing numeric enums and expecting names
Broken expectation:
type Base int
const (
A Base = iota
C
T
G
)
fmt.Println(A) // prints 0, not A
Fix it with a String() method.
4. Overusing iota when literal values are clearer
For DNA bases, this may be less expressive:
const (
A Base = iota
C
T
G
)
Sometimes this is clearer:
const (
A Base = 'A'
C Base = 'C'
T Base = 'T'
G Base = 'G'
)
Choose the representation that best matches the domain.
5. Forgetting a default case in String()
If an invalid value appears, your program should still behave predictably.
func (b Base) String() string {
switch b {
case A:
return "A"
case C:
return "C"
case T:
return "T"
case G:
return "G"
default:
return "Unknown"
}
}
Comparisons
| Approach | Example | Pros | Cons | Good for |
|---|---|---|---|---|
Typed int constants | type Base int + iota | Common Go pattern, easy to use in switch | Printed values are numeric unless String() is added | General enum-like values |
Typed byte constants | type Base byte + 'A' | Natural for character data, compact, easy to print | Still needs validation for invalid values | DNA bases, tokens, ASCII symbols |
Typed string constants | type Base string + "A" | Very readable, easy to serialize | Slightly more memory, less compact | Config values, APIs, readable text |
| Plain strings | "A" | Simple to start with | No type safety, easy to misuse | Quick scripts, prototypes |
iota vs explicit values
| Style | Example | Best use |
|---|---|---|
iota | A Base = iota | Sequential internal enum values |
| Explicit literals | A Base = 'A' | When the stored value should match the real-world symbol |
For DNA bases, explicit character literals are often more idiomatic and practical than iota.
Cheat Sheet
Enum-like pattern in Go
type MyType int
const (
ValueA MyType = iota
ValueB
ValueC
)
For DNA bases
type Base byte
const (
A Base = 'A'
C Base = 'C'
T Base = 'T'
G Base = 'G'
)
Validation method
func (b Base) Valid() bool {
switch b {
case A, C, T, G:
return true
default:
return false
}
}
String method
func (b Base) String() string {
return string(b)
}
Parse from string
func ParseBase(s string) (Base, error) {
if len(s) != 1 {
return 0, fmt.Errorf("invalid base: %s", s)
}
b := Base(s[0])
if !b.Valid() {
return 0, fmt.Errorf("invalid base: %s", s)
}
return b, nil
}
Key rule
Go does not have true restricted enums. Use:
- a named type
- typed constants
- validation when needed
Good default choice
- Use
int+iotafor general enums - Use
byteorstringwhen the domain value itself matters
FAQ
Is there a real enum type in Go?
No. Go usually uses custom named types with constants to represent enum-like values.
What is the most idiomatic way to emulate an enum in Go?
Define a named type and a fixed set of typed constants. Optionally add String() and validation methods.
Should I use iota for all enums in Go?
Not always. iota is great for sequential values, but explicit values are better when they reflect the real domain, such as 'A', 'C', 'T', and 'G'.
Is type Base byte better than type Base int for DNA bases?
Often yes, because DNA bases are naturally single characters and can be stored and printed conveniently.
Can invalid enum values still exist in Go?
Yes. A variable of a named type can still hold other underlying values unless you validate them.
How do I print enum values nicely in Go?
Implement a String() method, or use a character-based underlying type like byte when appropriate.
How should I validate enum values from user input?
Use a parsing function such as ParseBase(string) (Base, error) and reject anything outside the allowed set.
Mini Project
Description
Build a small Go program that models a DNA sequence using an enum-like Base type. The project demonstrates how to define a fixed set of allowed values, validate input, and work with a slice of strongly typed bases instead of raw strings.
Goal
Create a program that parses a DNA string, stores it as []Base, validates every character, and counts how many times each base appears.
Requirements
- Define a
Basetype for DNA values. - Create constants for
A,C,T, andG. - Write a function to parse a string into a slice of
Basevalues. - Reject invalid characters with a clear error.
- Count and print the number of each base in the sequence.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.