Understanding Custom Types in Go Programming
Custom types in Go allow you to create new types based on existing ones, adding type safety and domain-specific behavior to your code. This guide covers everything you need to know about working with custom types.
Type Definitions
Basic Type Definitions
// Define new types
type UserID int
type Email string
type Temperature float64
// Usage
var id UserID = 1
var email Email = "[email protected]"
var temp Temperature = 72.5
Struct Types
type Person struct {
ID UserID
Name string
Email Email
Age int
}
// Usage
person := Person{
ID: 1,
Name: "Alice",
Email: "[email protected]",
Age: 30,
}
Type Aliases
Basic Type Aliases
// Type alias declaration
type Celsius = float64
type Fahrenheit = float64
// Usage (no type safety)
var c Celsius = 25.0
var f Fahrenheit = c // Works, but might not be what you want
Type Definitions vs Aliases
// Type definition (new type)
type Celsius float64
// Type alias (same type)
type Fahrenheit = float64
func main() {
var c Celsius = 25.0
var f float64 = 77.0
// This won't compile
c = f // Error: cannot use f (type float64) as type Celsius
// This works
c = Celsius(f) // Explicit conversion needed
}
Methods on Custom Types
Adding Methods
type Celsius float64
func (c Celsius) ToFahrenheit() Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
func (c Celsius) String() string {
return fmt.Sprintf("%.2f°C", c)
}
// Usage
temp := Celsius(25.0)
fmt.Println(temp) // "25.00°C"
fmt.Println(temp.ToFahrenheit()) // 77.00
Method Sets
type Counter int
func (c *Counter) Increment() {
*c++
}
func (c Counter) Value() int {
return int(c)
}
// Usage
var c Counter
c.Increment() // Pointer receiver
fmt.Println(c.Value()) // Value receiver
Best Practices
1. Type Safety
// Good: Type safety for domain concepts
type UserID int64
type GroupID int64
func ProcessUser(id UserID) {}
func ProcessGroup(id GroupID) {}
// Can't accidentally mix them
uid := UserID(1)
gid := GroupID(1)
ProcessUser(uid) // OK
ProcessUser(gid) // Won't compile
2. Meaningful Types
// Good: Types convey meaning
type Duration time.Duration
type Money decimal.Decimal
type EmailAddress string
// Avoid: Unclear purpose
type X int
type Y string
3. Consistent Methods
// Good: Consistent method names
type Amount decimal.Decimal
func (a Amount) Add(b Amount) Amount
func (a Amount) Subtract(b Amount) Amount
func (a Amount) Multiply(b Amount) Amount
func (a Amount) Divide(b Amount) Amount
// Usage
price := Amount(10.99)
tax := Amount(0.88)
total := price.Add(tax)
Common Patterns
1. Enumerated Types
type Status int
const (
StatusPending Status = iota
StatusActive
StatusInactive
StatusDeleted
)
func (s Status) String() string {
names := map[Status]string{
StatusPending: "pending",
StatusActive: "active",
StatusInactive: "inactive",
StatusDeleted: "deleted",
}
return names[s]
}
2. Result Types
type Result struct {
Value interface{}
Error error
}
func (r Result) Unwrap() interface{} {
if r.Error != nil {
panic(r.Error)
}
return r.Value
}
// Usage
func divide(a, b float64) Result {
if b == 0 {
return Result{Error: errors.New("division by zero")}
}
return Result{Value: a / b}
}
3. Builder Pattern
type QueryBuilder struct {
table string
where []string
limit int
}
func (qb *QueryBuilder) From(table string) *QueryBuilder {
qb.table = table
return qb
}
func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
qb.where = append(qb.where, condition)
return qb
}
func (qb *QueryBuilder) Limit(limit int) *QueryBuilder {
qb.limit = limit
return qb
}
// Usage
query := new(QueryBuilder).
From("users").
Where("age > 18").
Limit(10)
Performance Considerations
1. Memory Layout
// Good: Efficient memory layout
type Record struct {
ID int64 // 8 bytes
Timestamp time.Time // 24 bytes
Name string // 16 bytes
Age int32 // 4 bytes
Active bool // 1 byte
}
// Bad: Inefficient memory layout
type Record struct {
Active bool // 1 byte + 7 padding
ID int64 // 8 bytes
Age int32 // 4 bytes + 4 padding
Name string // 16 bytes
Timestamp time.Time // 24 bytes
}
2. Method Receivers
// Value receiver (copy)
func (v Value) Process() {
// Good for small values
}
// Pointer receiver (reference)
func (v *Value) Process() {
// Good for large values or when modification is needed
}
Common Mistakes
1. Type Conversion vs Type Assertion
// Wrong: Unsafe type assertion
func process(i interface{}) {
s := i.(string) // Panics if i is not string
}
// Right: Safe type assertion
func process(i interface{}) {
s, ok := i.(string)
if !ok {
// Handle non-string case
return
}
// Use s
}
2. Method Receiver Consistency
// Wrong: Inconsistent receivers
type Counter int
func (c Counter) Value() int {
return int(c)
}
func (c *Counter) Increment() {
*c++
}
func (c Counter) Double() {
c *= 2 // Doesn't modify original
}
// Right: Consistent receivers
func (c *Counter) Double() {
*c *= 2 // Modifies original
}
3. Type Alias Misuse
// Wrong: Using type alias when type definition is needed
type UserID = int // Alias provides no type safety
// Right: Using type definition for domain types
type UserID int // New type with type safety
Next Steps
- Learn about interfaces
- Explore type embedding
- Study generics
- Practice with design patterns