Basic Object-Oriented Programming in Go


Is Go an object-oriented language? The official website offers an answer to this: "Yes and No". Basically Go does not intend to serve as an full object-oriented language like Ruby, but you can use it only in that way, like we do it in JavaScript. In fact, Go does not have the term "object", but there are other approaches to help us to do so.

Struct & Methods

In Ruby, we use class and def keywords to create a class and its methods like:

class Post
  def title
    "Title"
  end
end

In Go, we can do the same with methods:

type Post struct {
}

func (p *Post) Title() string {
  return "Title"
}

func main() {
  p := Post{}
  fmt.Println(p.Title())
}

As we declare the Post struct, we continue to declare the Title() function and designate it as a method of the type Post, so we can retrieve the title using p.Title() in the main function.

This data structure allows us to separate files like we do in a object-oriented language,  like in Ruby  we name a file post.rb as it contains solely the Post class.

Custom Type

Like in Ruby, we can create custom types from existing types:

type Num int

func (n *Num) Zero() int {
  return 0
}

func main() {
    var g Num = 7
    fmt.Println(g + g)    // => 14
    fmt.Println(g.Zero()) // => 0
}

The Num type includes all features of int type, and allows to add custom functions on it.

However, you may start wonder, we can create several types and make them inherit each other, so that is actually type inheritance.

Type Inheritance?

Nevertheless, the offical claims that there is no type inheritance in Go, which means the following code is broken:

type Num int
type Numm Num

func (n *Num) Zero() int {
  return 0
}

func main() {
    var g Num  = 7
    var h Numm = 8
    fmt.Println(g + g)    // => 14
    fmt.Println(h + h)    // => 16
    fmt.Println(h.Zero()) // => h.Zero undefined (type Numm has no field or method Zero)
}

The Numm type delegates the functions from int type, but does not delegates the Zero() method from Num type, which is an intentional design in Go. The purpose is to avoid the confusion in complicated delegations in a type-safe programming environment. There are still other ways to achieve so.

Type Embedding

One way to achieve type inheritance is to use type embedding. It is more like in Rails' ActiveRecord that we call another model using relatinoships:

class Post < ActiveRecord::Base
  belongs_to :author
end

class Author < ActiveRecord::Base
  has_many :posts
end

Post.first.author.name #=> "John"

Easy to understand. In Go, we can define this relationship within the struct:

type Post struct {
  Author Author
}

type Author struct {
  Name string
}

func main() {
    author := Author{"John"}
    post   := Post{author}
    fmt.Println(post.Author.Name // => "John"
}

When declaring a new type, we can implement another type into its fields. In this way, retrieving the author's name with post.Author.Name is more instictive and understandable with this object-oriented idea. Futhermore, we can use "Anonymous Field" to get closer to type inheritance.

Anonymous Field

The way anonymous field works is to embed a specific type into a field but does not give it a name. In this approach, the new type will have the methods you have defined for another type:

type Post struct {
  Title string
}

func (post *Post) Author() {
  fmt.Println("John")
}

type Article struct {
  Post
}g

func main() {
  post    := Post{"Golang"}
  post.Title               // => "Golang"
  post.Author()            // => "John"
  artirle := Article{}
  article.Title            // => ""
  article.Author()         // => "John"
  another_artirle := Article{Post{"Golang"}}
  another_article.Title    // => "Golang"
}

Now the article has the Atuhor() method defined in Post type. This looks great! Except that it is only a way to get the methods but not actually type inheritance. Unlike Ruby in which we can do String.new.is_a?(Object) and get true, if you check the type of the article, it has nothing to do with type Post. They are still separate types.

Also, this technique may introduce confusion if the same method is defined in two structs like:

type Post struct {
  Title string
}

func (post *Post) Author() {
  fmt.Println("John")
}

type Article struct {
  Post
}

func (post *Post) Author() {
  fmt.Println("Martin")
}

func main() {
  article := Article{}
  article.Author() // => "Martin"
}

The Author() is defined both in Post and Article structs, so the atricle.Author() reads the method from the latter first. This is a typical confusion we see in other OO language if we are being careless designing the application.