Learn › Go Programming

Structs & Interfaces

Define custom types with structs, implement polymorphism through interfaces, and use embedding for composition.

Structs

Structs are Go's primary mechanism for defining custom composite types. They group together zero or more fields of different types into a single entity, similar to classes in object-oriented languages but without inheritance. Struct fields are accessed using dot notation and can be exported (capitalized) or unexported (lowercase). Structs can be initialized using named fields for clarity or positional arguments for brevity.

package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
    Age       int
    Email     string
}

func main() {
    // Named field initialization
    p1 := Person{
        FirstName: "Alice",
        LastName:  "Smith",
        Age:       30,
        Email:     "alice@example.com",
    }

    // Pointer to struct
    p2 := &Person{
        FirstName: "Bob",
        LastName:  "Jones",
        Age:       25,
    }

    // Access and modify fields
    fmt.Println(p1.FirstName, p1.LastName)
    p2.Age = 26 // Go auto-dereferences pointers
    fmt.Printf("%s is %d years old\n", p2.FirstName, p2.Age)

    // Zero-value struct
    var p3 Person
    fmt.Println("Zero:", p3.FirstName, p3.Age)
}

Methods on Structs

Methods give structs behavior by associating functions with a particular struct type through a receiver parameter. Pointer receivers are preferred when the method needs to modify the struct or when the struct is large and copying would be expensive. It is conventional to use pointer receivers for all methods on a type if any one method requires a pointer receiver. Methods with value receivers can be called on both values and pointers, but methods with pointer receivers can only be called on addressable values.

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func (r Rectangle) String() string {
    return fmt.Sprintf("Rectangle(%.1f x %.1f)", r.Width, r.Height)
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println(rect)
    fmt.Printf("Area: %.1f, Perimeter: %.1f\n", rect.Area(), rect.Perimeter())

    rect.Scale(2)
    fmt.Println("After scaling:", rect)
    fmt.Printf("New area: %.1f\n", rect.Area())
}

Interfaces

Interfaces in Go are satisfied implicitly — a type implements an interface simply by implementing all of its methods, with no explicit declaration required. This approach enables loose coupling and makes it easy to define small, focused interfaces. The Go community favors small interfaces, often with just one or two methods, following the principle that the bigger the interface, the weaker the abstraction. The empty interface 'interface{}' (or 'any' in Go 1.18+) is satisfied by all types and is used when you need to handle values of unknown type.

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

type Square struct {
    Side float64
}

func (s Square) Area() float64      { return s.Side * s.Side }
func (s Square) Perimeter() float64 { return 4 * s.Side }

func printShape(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Square{Side: 4},
    }
    for _, s := range shapes {
        printShape(s)
    }
}

Embedding and Type Assertions

Go uses struct embedding as a form of composition, allowing one struct to include another and automatically promoting its fields and methods. This is Go's answer to inheritance and is generally preferred over deep type hierarchies. Type assertions provide access to the concrete type underlying an interface value and are written as 'value.(Type)'. The comma-ok form of type assertions prevents panics when the assertion fails, and type switches let you branch on the dynamic type of an interface.

package main

import "fmt"

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

type Dog struct {
    Animal // Embedded struct
    Breed  string
}

func (d Dog) Fetch() string {
    return d.Name + " fetches the ball!" // Name is promoted
}

func describe(i interface{}) {
    // Type switch
    switch v := i.(type) {
    case string:
        fmt.Printf("String: %s\n", v)
    case int:
        fmt.Printf("Int: %d\n", v)
    case Dog:
        fmt.Printf("Dog: %s (%s)\n", v.Name, v.Breed)
    default:
        fmt.Printf("Unknown: %T\n", v)
    }
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Rex"},
        Breed:  "German Shepherd",
    }
    fmt.Println(d.Speak()) // Promoted method
    fmt.Println(d.Fetch())

    describe("hello")
    describe(42)
    describe(d)
}

← Arrays, Slices & Maps · Error Handling →