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)
}