Is Go an object-oriented programming language?

Golang has been open source for 13 years, and in the recent TIOBE ranking of programming languages for March 2023, Go once again broke into the top 10, moving up two spots from Go’s ranking at the end of 2022.

Go improves its ranking by 2 places at the end of 2022

Many first-time Go developers, many of whom come from the OO (object-oriented) language camp like Java, Ruby, etc., ask the first question they ask after learning Go: Is Go an OO language? In this blog post, we’ll explore this.

I. Traceability

In the recognized Go “bible” The Go Programming Language, there is a kinship chart between Go and its major ancestor programming languages.

Go’s kinship chart with its major ancestor programming languages

From the diagram, we can clearly see the “inheritance lineage” of the Go language.

  • Borrowed expression syntax, control statements, basic data types, value parameter passing, pointers, etc. from C.
  • The syntax for package, package import and declaration is borrowed from the Oberon-2 language, while Object Oberon provides the syntax for method declaration.
  • The CSP-based concurrency syntax was borrowed from the Alef language as well as the Newsqueak language.

We have seen that Go does not take its cues from pure object-oriented languages such as Simula, SmallTalk, etc., in terms of Go ancestry traceability.

Go was born in 2007 and the code was open sourced in 2009, a time when object-oriented languages and the OO paradigm were in vogue. However, Go designers felt that the classical OO inheritance system did not seem to have much benefit for programming and extension, and also brought more restrictions, so the official version did not support the classical OO syntax, that is, the three main OO features of encapsulation, inheritance and polymorphism based on class and object implementation.

But does this mean that Go is not an OO language? Neither! Object Oberon, with its object-oriented mechanism, is also one of the ancestors of Go, although the OO syntax of Object Oberon is again quite different from the syntax we commonly see today.

I also consulted ChatGPT on this issue and received the following response.

Go is a multi-paradigm programming language that supports both procedural and object-oriented programming (OOP) concepts. However, Go does not support the full range of object-oriented features found in languages like Java or C++, such as inheritance and method overriding. Instead, Go uses interfaces and composition to achieve similar functionality. Therefore, some would say that Go is not a strictly object-oriented programming language, but rather a procedural language with some object-oriented capabilities.

So is there an official Golang response to this question? Yes, let’s take a look.

II. The official voice

Go official brief response in FAQ on whether Go is an OO language.

Is Go an object-oriented language?

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

“Yes and no”! We see that Go officials have given a moderate answer of “no harm to either side”. So what does the Go community think? Let’s take a look at some typical representatives of the Go community.

III. Community Voices

Jaana Dogan and Steve Francia, both former Go core team members, had their own opinions on the question of whether Go is an OO language before they joined the Go team.

The view given by Jaana Dogan in “The Go type system for newcomers” is that Go is considered as an object-oriented language even though it lacks type hierarchy, i.e. “Go is considered as an object-oriented language even though it lacks type hierarchy”.

A bit earlier is Steve Francia’s 2014 article “Is Go an Object Oriented language?” in which he concludes that Go, object-oriented programming without objects or inheritance, can also be called the “objectless” OO programming model.

The two express different wording but have the same meaning, i.e., Go supports object-oriented programming, but not by providing the classic class, object, and type hierarchies.

So in what way does Go actually implement OOP support? Let’s keep looking!

IV. Go’s “objectless” OO programming

The three main features of classical OO are encapsulation, inheritance and polymorphism, and here we see how they correspond in Go.

1. Encapsulation

Encapsulation is the “packaging” of data and methods to manipulate it into an abstract data type that hides the implementation details and all data can only be accessed and manipulated by the exposed methods. An instance of this abstract data type is called an object. Classical OO languages such as Java and C++ express the concept of encapsulation through classes (class), and map objects through instances of classes. People familiar with Java must remember the title of the second chapter of the book Thinking in java: “Everything is an object”. In Java, all properties and methods are defined in classes.

Go doesn’t have classes, so how does the concept of encapsulation come into play? When beginners from OO enter the Go world, they like to “match up” what syntax element in Go is closest to class! So they found the struct type.

The struct type in Go provides the ability to abstract from real-world aggregation. The definition of a struct can contain a set of fields, which you can also think of as properties if you look at it from an OO perspective, and we can also define methods for the struct type. In the following example we define a struct type named Point, which has an exported method Length.

1
2
3
4
5
6
7
type Point struct {
    x, y float64
}

func (p Point) Length() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

We see that, syntactically, unlike the methods of a class declared in classical OO, the declaration of a Go method does not need to be placed in curly brackets declaring a struct type. the link between the Length method and the Point type is a syntactic element called the receiver argument.

So, is struct the class that corresponds to the class in classical OO? Yes and no! In terms of data aggregation abstraction, it seems so, struct types can have multiple fields of heterogeneous types representing different abstraction capabilities (e.g. integer type int can be used to abstract the length of a real-world object, string type fields can be used to abstract the name of a real-world object, etc.).

But from the point of view of having methods, not only struct types, but all other named types in Go except the built-in types can have their own methods, even a new type MyInt whose underlying type is int.

1
2
3
4
5
type MyInt int

func(a MyInt)Add(b int) MyInt {
    return a + MyInt(b)
}

2. Inheritance

As mentioned earlier, Go designers reevaluated support for the classical OO syntactic concepts at the beginning of Go, eventually abandoning support for things like classes, objects, and class inheritance hierarchies.

When it comes to inheritance in OO, what comes to mind is that the child class inherits the properties and methods of the parent class, but Go doesn’t have an explicit inheritance syntax like the Java extends keyword, but Go also provides support for “inheritance” in a different way. This support is type embedding, see an example.

 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
33
34
35
36
type P struct {
    A int
    b string
}

func (P) M1() {
}

func (P) M2() {
}

type Q struct {
    c [5]int
    D float64
}

func (Q) M3() {
}

func (Q) M4() {
}

type T struct {
    P
    Q
    E int
}

func main() {
    var t T
    t.M1()
    t.M2()
    t.M3()
    t.M4()
    println(t.A, t.D, t.E)
}

We see that type T “inherits” the export methods (M1 to M4) and export fields (A, D) of P and Q by embedding two types P and Q.

In fact, this “inheritance” mechanism in Go is not inheritance in classical OO, there is no “kinship” between the outer type (T) and the embedded types (P, Q), the exported fields and methods of P, Q are just elevated to the fields and methods of T. It is essentially a combination, an implementation of the delegate pattern in the combination. T is just a delegate that provides all the methods it can proxy, such as the M1~M4 methods in the example. When the outside world initiates a call to T’s M1 method, T delegates the call to its internal P instance to actually execute the M1 method.

The relationship between T and P and Q is not is-a, but has-a, as understood by the classical OO theory of discourse.

3. Polymorphism

Polymorphism in classical OO refers in particular to Runtime Polymorphism, which means that when a method is called, a different type of method implementation is called depending on the type of the actual object calling the method.

The following is an example of a typical polymorphism in C++.

 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
33
#include <iostream>

class P {
        public:
                virtual void M() = 0;
};

class C1: public P {
        public:
                void M();
};

void C1::M() {
        std::cout << "c1.M()\n";
}

class C2: public P {
        public:
                void M();
};

void C2::M() {
        std::cout << "c2.M()\n";
}

int main() {
        C1 c1;
        C2 c2;
        P *p = &c1;
        p->M(); // c1.M()
        p = &c2;
        p->M(); // c2.M()
}

The code is relatively clear, with a parent class P and two subclasses C1 and C2. the parent class P has a virtual member function M, and the two subclasses C1 and C2 each override the M member function. In main, we declare a pointer to the parent class P and then assign the object instances of C1 and C2 to p and call the M member function respectively. From the result, the actual function called by p at runtime will call M for C1 and C2 respectively depending on the actual type of the object instance it points to.

Obviously, the classical OO implementation of polymorphism relies on the type hierarchy, so how does Go, which has no type hierarchy, implement polymorphism?

Go uses interfaces to implement polymorphism!

Compared to classic OO languages, Go places more emphasis on behavior aggregation and consistency than on data. Therefore Go provides support for duck typing-like type adaptation based on behavior aggregation, but compared to dynamic languages like ruby, Go’s static typing mechanism also ensures type safety when applying duck typing.

Go’s interface types are essentially a collection of methods (a collection of behaviors). A type that implements all the methods of an interface type can be assigned to the interface type as a dynamic type. A call to a method via a variable of that interface type is actually a call to the method implementation of its dynamic type. Look at the following example.

 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
type MyInterface interface {
    M1()
    M2()
    M3()
}

type P struct {
}

func (P) M1() {}
func (P) M2() {}
func (P) M3() {}

type Q int
func (Q) M1() {}
func (Q) M2() {}
func (Q) M3() {}

func main() {
    var p P
    var q Q
    var i MyInterface = p
    i.M1() // P.M1
    i.M2() // P.M2
    i.M3() // P.M3

    i = q
    i.M1() // Q.M1
    i.M2() // Q.M2
    i.M3() // Q.M3
}

Go, a polymorphic implementation that does not require a type inheritance hierarchy and is low-coupling, is lighter and easier to use!

V. Gopher’s “OO thinking”

By now, those of you from the classic OO camp have found the reason why you “felt awkward” when you started with Go! This “awkwardness” lies in the difference between the way Go supports OO and the classic OO language: the classical OO mindset requires a hierarchy of inheritance that Go does not have and does not need.

To transform into authentic Gopher OO thinking is not difficult, that is, “prefer interfaces, prefer combinations, and change the habit of is-a thinking to has-a thinking.

VI. Summary

It’s time to give some concluding remarks.

  • Go supports OO, just with a syntax other than classic OO and a type system with hierarchies.
  • Go supports OO, it just needs to think differently when using it.
  • The way to think about playing with OO in Go is: “prioritize interfaces, prioritize combinations”.

VII. Ref

  • https://tonybai.com/2023/03/12/is-go-object-oriented/