There’s no class, no inheritance, no polymorphism in Go,
but we can still achieve OOP with struct, method and interface.

The only way to abstract is composition.

Structs

There is NO CLASS in Go, but structs is supported.
They are also useful for grouping data together.

package main
import "fmt"
 
type person struct {
 name string
 age  int
}
// Define a struct `person` with two fields: `name` (string) and `age` (int)
 
func newPerson(name string, age int) *person {
 p := person{age: age}
 p.name = "Mr. " + name
 return &p
}
// Define a constructor.
// To initialize a struct, use `structName{structField: value, ...}`
// Access a field in a struct by `structVariable.structField`
// Return the pointer of struct variable defined and initialized in constructor.
// There's GC in Go, we don't need to worry about memory leaking.
 
func main() {
 fmt.Println(person{"Alice", 30})
 fmt.Println(person{name: "Bob", age: 35})
 fmt.Println(person{name: "Charlie"})        // Omitted fields will be zero-valued
 fmt.Println(&person{name: "Dave", age: 40}) // & prefix yields a pointer to the struct
 fmt.Println(newPerson("Eve", 25))           // Idiomatic way with constructor
 
 s := person{"Frank", 28}
 fmt.Println(s.name) // Access struct fields with dot notation
 
 sp := &s
 fmt.Println(sp.age)
 // Structs are mutable
 
 // Access struct fields through pointer with dot notation,
 // it will be dereferenced automatically.
 sp.age = 29
 fmt.Println(sp.age)
 
 dog := struct {
  name string
  age  int
 }{
  "Buddy",
  3,
 }
 fmt.Println(dog.name) // Anonymous struct
}
/*
{Alice 30}
{Bob 35}
{Charlie 0}
&{Dave 40}
&{Mr. Eve 25}
Frank
28
29
Buddy
*/

Struct Embedding

Go supports embedding of structs and interfaces to express a more seamless composition of types.

package main
import "fmt"
 
type base struct {
 num int
}
 
func (b base) describe() string {
 return fmt.Sprintf("base with num=%v", b.num)
}
 
type container struct {
 base
 str string
} // A container embeds a `base`. An embedding looks like a field without a name.
 
func main() {
 co := container{
  base: base{
   num: 1,
  },
  str: "A container",
  // We have to initialize the embedding explicitly.
  // Here the embedded type serves as the field name.
 }
 
 fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str)
 // We can access the base’s fields directly on co, e.g. co.num.
 
 fmt.Println("also can be accessed: ", co.base.num)
 // Not recommended.
 
 fmt.Println("describe: ", co.describe())
 // Since container embeds base, the methods of base also become methods of a container.
 
 type describer interface {
  describe() string
 }
 
 var d describer = co
 fmt.Println("describer: ", d.describe())
 // The struct embedding another will implements the interface inner one implemented.
}
/*
co={num: 1, str: A container}
also can be accessed:  1
describe:  base with num=1
describer:  base with num=1
*/

Methods

package main
import "fmt"
 
type rect struct {
 height, width int
}
 
func (r *rect) area() int {
 return r.height * r.width
}
// Define a method has a *receiver type* of `*rect`
 
func (r rect) perim() int {
 return 2*r.height + 2*r.width
}
// Method can also be defined with a *receiver type* of `rect` (not a pointer)
 
// If use pointer, we can change the state of the struct,
// decrease the cost of copying the struct when the struct is large.
// If use value, it cannot be changed, and will be copied when the method is called.
 
func main() {
 r1 := rect{10, 5}
 fmt.Println("area: ", r1.area())
 fmt.Println("perim: ", r1.perim())
 
 r2 := &rect{3, 7}
 fmt.Println("area: ", r2.area())
 fmt.Println("perim: ", r2.perim())
 // But whatever which is used, value and pointer can call these methods
 // by automatically dereferencing the pointer or taking the address of the value.
}
/*
area:  50
perim:  30
area:  21
perim:  20
*/

Interface

Go supports duck type through interfaces.

package main
import (
 "fmt"
 "math"
)
 
type geometry interface {
 area() float64
 perim() float64
}
 
type rect struct {
 width, height float64
}
 
type circle struct {
 radius float64
}
 
func (r rect) area() float64 {
 return r.width * r.height
}
 
func (r rect) perim() float64 {
 return 2*r.width + 2*r.height
}
 
// To implement an interface, we just need to implement all the methods in the interface
// without the need of any explicit declaration. No `implement` keyword is needed in Go.
 
func (c circle) area() float64 {
 return math.Pi * c.radius * c.radius
}
 
func (c circle) perim() float64 {
 return 2 * math.Pi * c.radius
}
 
func measure(g geometry) {
 fmt.Println(g)
 fmt.Println("Area: ", g.area())
 fmt.Println("Perimeter: ", g.perim())
}
 
// If a variable has an interface type, then we can call methods that are in the interface.
 
func detectShape(g geometry) {
 switch t := g.(type) {
 case rect:
  fmt.Println("This is a rectangle with width", t.width, "and height", t.height)
 case circle:
  fmt.Println("This is a circle with radius", t.radius)
 }
}
 
// Sometimes, runtime type is important.
// As what we had introduced in the previous section,
// we can use type assertion to get the dynamic type of an interface variable.
 
func main() {
 r := rect{width: 3, height: 4}
 c := circle{radius: 5}
 
 measure(r)
 measure(c)
 
 detectShape(r)
 detectShape(c)
}
/*
{3 4}
Area:  12
Perimeter:  14
{5}
Area:  78.53981633974483
Perimeter:  31.41592653589793
This is a rectangle with width 3 and height 4
This is a circle with radius 5
*/

Notice that it is different between a struct implements an interface and a pointer of that struct do so.

Pointer vs. Value Receiver in Go Interfaces:

  • Pointer Receiver (*T): Only a pointer (&T) can satisfy the interface. Passing a value (T) will fail because Go prevents modifying a temporary, copied value.
  • Value Receiver (T): Both a value (T) and a pointer (&T) can satisfy the interface. If a pointer is passed, Go automatically dereferences it safely.

Rule of thumb: When in doubt, use a pointer receiver.
It avoids copying large data and guarantees that identity and structural modifications are preserved.

When a method uses a pointer receiver, the struct itself does NOT implement the interface,
only the pointer type does.

Therefore, you MUST return the pointer (&T), never the struct (T).

In languages like Java, a class always implements an interface.
In Go, types implement interfaces, and MyStruct and *MyStruct are different types.

func (e *MyError) Error() string  // Only *MyError implements error!
ActionWhat happens?Why?
return &MyError{}Valid*MyError implements the interface.
return MyError{}Compile ErrorMyError does NOT implement the interface.

Generics

package main
import "fmt"
 
func MySliceIndex[S ~[]E, E comparable](s S, v E) int {
 // It takes type `S` which is a slice (or a type defined based on slice, declared by by `~`)
 // that contains elements comparable.
 for i := range s {
  if v == s[i] {
   return i
  }
 }
 return -1
}
 
// Here we defined two generics type.
// The node of a linked list
// and a singly-linked list type taking the reference of head and tail.
type element[T any] struct {
 val  T
 next *element[T]
 // Due to `element` is a generics type,
 // we can specify its type by `[T]`
}
 
type List[T any] struct {
 head, tail *element[T]
}
 
// Then we can define methods with generics type for generics struct.
// Just like what we'd done for normal struct, but note that the type is List[T], not List.
func (lst *List[T]) Push(elem T) {
 if lst.tail == nil {
  lst.head = &element[T]{val: elem} // Initialize a generics type variable by `[T]`
  lst.tail = lst.head
 } else {
  lst.tail.next = &element[T]{val: elem}
  lst.tail = lst.tail.next
 }
}
 
func (lst *List[T]) AllElement() []T {
 var ret []T
 for ptr := lst.head; ptr != nil; ptr = ptr.next {
  ret = append(ret, ptr.val)
 }
 return ret
}
 
func main() {
 s := []string{"foo", "bar", "zoo"}
 
 fmt.Println("The index of bar: ", MySliceIndex(s, "bar"))
 // When invoking generic functions, we can often rely on type inference.
 // Note that we don’t have to specify the types for S and E when calling SlicesIndex.
 // The compiler infers them automatically.
 
 _ = MySliceIndex[[]string, string](s, "zoo")
 // Though we could also specify them explicitly.
 
 lst := List[int]{}
 lst.Push(1)
 lst.Push(2)
 lst.Push(3)
 fmt.Println("lst: ", lst.AllElement())
}
 
/*
The index of bar:  1
lst:  [1 2 3]
*/