1. go
  2. /data structures
  3. /custom-types

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

  1. Learn about interfaces
  2. Explore type embedding
  3. Study generics
  4. Practice with design patterns

Additional Resources