Golang Decoupling, Embedding and Exporting

Posted by Henry Du on Sunday, November 8, 2020

Golang Decoupling, Embedding and Exporting

Decoupling

Golang does not have a concept of object-oriented programming (OOP), including the concept of class or a class-based inheritance. Rather, Golang has its own way to interpret the regular object-oriented languages - decoupling.

In traditional OOP design, one class contains member variables, either public or private. It also contains member functions, either public or private. In order to reuse the class member function, another class has to inherit the class. Golang provides an approaches to make design more efficient: Decouple a concrete data (struct) with a set of member functions (interface).

Decoupling separates concrete data and behaviors. The concrete data can be defined as an object data structure such as struct. The behaviors are defined as a set of member functions in the type of interface.

The good thing about it is, we are able to implement the same interface methods for different concrete data, either with a value receiver, or with a pointer receiver.

The following example illustrates the polymorphism of Golang.

package main

import (
	"fmt"
)

type reader interface {
	read() (int, error)
}

type file struct {
	name        string
	fileContent string
}

func (f *file) read() (int, error) 
	content := "file contents"
	f.fileContent = content
	return len(content), nil
}

type pipe struct {
	name        string
	pipeMessage string
}

func (p *pipe) read() (int, error) {
	msg := `{name: "Henry", title: "developer"}`
	p.pipeMessage = msg
	return len(msg), nil
}

func main() {
	f := file{
		name: "data.json",
	}
	p := pipe{
		name: "pipe_message",
	}

	retrieve(&f)
	fmt.Println(f.fileContent)
	retrieve(&p)
	fmt.Println(p.pipeMessage)
}

func retrieve(r reader) error {
	len, _ := r.read()
	fmt.Println(fmt.Sprintf("read %d bytes", len))
	return nil
}

After retrieve function passes the concrete data file struct as a reader interface, what happens is, the interface r's type is pointed to a iTable which contains file type, and its behavior’s pointer, which points to read method. The interface r's value is copied from the real concrete data of file struct.

When the second time call retrieve, since the concrete data pipe is passed as a reader interface, then, the r interface type is changed to pipe and the value is copied from pipe.

We can think that the Golang interface is a pointer which points to a component with Type pointer and Value pointer. Golang provides a way reflect to extract this type and value. We may have another blog to demonstrate it.

Embedding

We can embed one concrete data struct to another concrete data struct. Or, Embedded a set of member functions (interface) in a concrete data (struct). It actually creates the relationship between two objects. The inner type can be prompted to the outer type object. Then, the outer type concrete data can directly use the inner type behaviors. The following is an example

package main

import (
    "fmt"
)

type mailer interface {
    notify()
}

type user struct {
    name string
    email string
}

func (u *user) notify() {
    fmt.Println("sending email")
}

type admin struct {
    user
    level int
}

func main() {
    user := user{
        name: "Henry Du",
        email: "henrydu.com@gmail.com",
    }

    ad := &admin{
        user: user,
        level: 8,
    }

    ad.notify()
}

Exporting

Exporting is more or less like capsulation pieces of Golang. For example, We could define enumeration type which contains an integer. Based on the exported named type, we could implement an interface to have several behaviors to be used. Then, we could import this named type from other package.

As we have already known, only first-letter-capital named type or field type can be exported to other package in Golang.

type RunningMode uint32

const (
	LBMode RunningMode = iota
	ServerMode
)

const (
	LBModeStr = "Load Balancer"
	ServerModeStr   = "Server"
)

func (r RunningMode) IsServerMode() bool {
	switch r {
	case LBMode:
		return false
	case ServerMode:
		return true
	}
	return false
}

func (r RunningMode) String() string {
	switch r {
	case LBMode:
		return LBModeStr
	case ServerMode:
		return ServerModeStr
	}
	return "invalid running mode"
}