Go is a strongly typed, static programming language. Almost every line of code we program in Go is inextricably linked to a type. Therefore, to learn Go in depth, we must first have a comprehensive and in-depth understanding of the Go type system, which gives us a holistic view of the specific type-related aspects of the Go language.

1. What is the type system

As a Gopher with some Go programming experience, you have some knowledge of types in the Go language, for example: Go has built-in native integer types, floating point types, complex types, string types, function types, and provides composite types such as arrays, slices, maps, structs, channels, and interface types that represent behavioural abstractions. With the type keyword provided by Go, we can also customise types and much more.

So have you ever thought about the question: why are there types? What benefits can types bring? Looking back at the history of programming languages (see below), we see that types are an important abstraction that distinguishes high-level languages from machine languages and low-level languages.

Type systems

From the machine’s point of view, data of any type is 0101 binary data, but it is very difficult and inefficient for programmers to code directly in machine language; assembly language takes the hierarchy up to multi-byte data-oriented coding, where the operands of assembly instructions are fixed-length bytes, e.g. movb operates on one byte, movl operates on four bytes. Assembly instructions don’t care what data is actually stored, they just move a specific length of data between addresses. Clearly the level of abstraction in assembly is still not high and it is still very difficult and inefficient to write programs directly in sinks.

High-level languages are advanced because they create the important abstraction of types, which shields the developer from complex representations of machine-level data. The complex byte and bit manipulation beneath the type is facilitated by the compiler and runtime of the high-level language, and the developer only needs to code type-oriented, meaning that the type becomes the ‘interface’ between the developer and the compiler.

Types become the “interface” between the developer and the compiler

Type-oriented programming requires the developer to understand the capabilities of types, the abstractions they represent and to follow the rules/constraints on the use of types. The type determines the range of values you can store in instances of the type; the type determines the operations you can perform on the type; the type determines the storage space required for variables of the type; the type determines the method of making connections to other types: combination, ‘inheritance’ or interface implementation, etc.

So who gives these abilities, rules and constraints to types? Yes, it is the type system of the programming language!

The type system is at the heart of a high-level language. It is found in the language specification, which specifies the capabilities, rules and constraints of types to the developer; it is found in the compiler, which ensures that the developer uses the types correctly and it is also found in the language runtime, which provides dynamic capabilities for types such as polymorphism.

It is fair to say that high-level programming languages use type systems to empower types and manage them. However, the design and implementation of the type system varies considerably from language to language, so what makes the Go language’s type system different? Let’s take a closer look at Go’s type system.

2. Go’s type system

Below we illustrate the capabilities and shortcomings of the Go type system in terms of type definition, type inference, type checking, type combination, and so on.

2.1. Type definitions

As you know Go supports almost all types, here is a screenshot of the list of type categories in the Go spec.

Type classification in Go spec

Go also supports custom types defined using the type keyword and type alias.

1
2
3
4
5
6
7
8
type CustomType int // The underlying type is a custom type CustomType of the native type int

type S struct {
    a int
    b string
} // Custom type S based on struct

type IntAlias = int // Type alias for int IntAlias

A custom type and its underlying type are two completely different types, and a type alias does not introduce a new type and is equivalent to the original type.

However there are two types common in other languages that the Go type system does not support, one is the union union type, in which all its fields share the same memory space.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// C

// Define a union type named num
// Its three members m, ch and f share the same memory space
// The C compiler allocates memory space for num type variables with the size of the largest field
union num {
    int m;
    char ch;
    double f;
};
union num a, b, c; // Declare three variables of type union

The other type is the enum enum type. However enum types can be emulated to some extent with const (optionally with iota).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// C
enum Weekday {
        SUNDAY,
        MONDAY,
        TUESDAY,
        WEDNESDAY,
        THURSDAY,
        FRIDAY,
        SATURDAY
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Go
type Weekday int

const (
        Sunday Weekday = iota
        Monday
        Tuesday
        Wednesday
        Thursday
        Friday
        Saturday
)

Go supports generics from version 1.18 onwards, giving the Go type system the ability to define types and functions with type parameters.

2.2. Type inference

The Go type system supports automatic type inference capabilities, where the compiler can infer the type of a variable or function without us having to explicitly specify it.

1
2
3
var s = "hello" // s is of type string
a := 128        // a is of type int
f := 4.3567     // f is of type float64

In addition to support for normal type inference, Go also supports inference on autotyped arguments for generic types, here is an example from the go spec.

1
2
3
4
func scale[Number ~int64|~float64|~complex128](v []Number, s Number) []Number

var vector []float64
scaledVector := scale(vector, 42)

In the example, the compiler can automatically infer that the type parameter Number for scale is of type float64 by the type of the argument passed in during the scale call.

2.3. Type checking

Go is a strongly typed static programming language, meaning that every variable must have its type declared before it can be used. Once we have a type, we can manipulate it according to the valid operations specified by the Go type system for that type.

The Go compiler and runtime checks variable types during compilation and at runtime respectively, to ensure that operations are only used on the correct type and that the rules of the type system are followed by the program, to ensure type safety etc.

Go is a strongly typed language and has no implicit type conversions, all type conversions are implemented as explicitly intended explicit type conversions, the Go compiler checks for type conversions during compilation and only the two types that are underlying type compatible can be explicitly transformed.

1
2
3
4
5
6
7
8
9
type T1 int
type T2 struct{}

var i int = 5
var t T1
var s T2
t = i     // Error, not the same type
t = T1(i) // ok, underlying type compatible
s = T2(t) // Error, underlying type incompatibility

In addition to static checks during compilation, the Go type system also supports dynamic type checking at runtime, for example: checking that the type instance passed to an interface variable implements that interface; checking index bounds of array and slice types at runtime to ensure that indexes do not cross bounds, ensuring memory safety, etc.

However Go also provides means of bypassing type system checks, such as unsafe.Pointer and reflection.

2.4. Type Combination

Go is not a classical OO language, its types can have their own methods, but Go does not provide the complex inheritance hierarchy of classical OO, there are no parents, no subclasses and no constructors for type initialisation. In Go’s type system, the only way to create combinations between types is to combine them, and through type embedding we can implement various combinations, either by embedding non-interface types or by embedding interfaces to define the new combined types.

Type combination allows us to combine various types together to collectively provide aggregated behaviour to the outside world, including polymorphic capabilities.

The standard polymorphic capabilities in Go are implemented by the interface type, and methods are dispatched at runtime depending on the specific type passed to the interface type variable. For example, the Quack in the AnimalQuackInForest example below is dispatched depending on the specific type instance passed in, and is dispatched to Duck.Quack, Dog.Quack and Bird.Quack in turn.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type QuackableAnimal interface {
    Quack()
}

type Duck struct{}

func (Duck) Quack() {
    println("duck quack!")
}

type Dog struct{}

func (Dog) Quack() {
    println("dog quack!")
}

type Bird struct{}

func (Bird) Quack() {
    println("bird quack!")
}                         

func AnimalQuackInForest(a QuackableAnimal) {
    a.Quack()
}                         

func main() {
    animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)}
    for _, animal := range animals {
        AnimalQuackInForest(animal)
    }
}

The implementation relationship between the type and the interface is implicit, the type does not need to be explicitly informed of the type of interface to be implemented using the implements keyword.

Functions in Go are first class citizens, and function types can also exhibit some runtime polymorphism, with the final execution of an instance of a function type depending on the value of the function object passed in at runtime.

3. Summary

Go provides a powerful and interesting type system, although Go does not provide enum, union types, nor does it support operator overloading, function overloading, structured error handling, and optional/default function arguments. This is not unrelated to the decision made by Go’s designers to keep Go simple. The type system is also instrumental in keeping Go a safe language.

If you are serious about Go programming, you should invest time in understanding its type system and its peculiarities, which will be well worth your time.

4. Ref

  • https://tonybai.com/2022/12/18/go-type-system/
  • https://thevaluable.dev/type-system-software-explained-example/
  • https://rakyll.org/typesystem/
  • https://code.tutsplus.com/tutorials/deep-dive-into-the-go-type-system-cms-29065