Software Development

First Steps Into the World of Go

Since developers should learn a new programming language every year, I felt it was about time for me to dive into something new and I decided on Go.

The good news is that Go has awesome documentation to get you started. More good news is that Go has a mature ecosystem of tools, including support for getting dependencies, formatting, and testing.

There is also an Eclipse plugin that supports Go. Although it isn’t as complete as support for Java (e.g. very few quick fixes or refactorings), it’s a lot better than for some other languages I’ve tried.

The best way to learn anything is to learn by doing, so I started with a simple kata: develop a set type. The standard library for Go doesn’t offer sets, but that’s besides the point; I just want to learn the language by building something familiar.

So let’s get started with the first test. The convention in Go is to create a file with the name foo_test.go if you want to test foo.go.

package set
 
import (
  "testing"
)
 
func TestEmpty(t *testing.T) {
  empty := NewSet()
  if !empty.IsEmpty() {
    t.Errorf("Set without elements should be empty")
  }
}

(WordPress doesn’t currently support Go syntax highlighting, so the func keyword is not shown as such.)

There are several things to note about this piece of code:

  • Go supports packages using the package statement
  • Statements are terminated by semi-colons (;), but you can omit them at the end of the line, much like in Groovy
  • You import a package using the import statement. The testing package is part of the standard library
  • Anything that starts with a lower case letter is private to the package, anything that starts with an upper case letter is public
  • Code in Go goes inside a function, as indicated by the func keyword
  • Variable names are written before the type
  • The := syntax is a shorthand for declaring and initializing a variable; Go will figure out the correct type
  • Go doesn’t have constructors, but uses factory functions to achieve the same
  • if statements don’t require parentheses around the condition, but do require braces
  • The testing package is quite small and lacks assertions. While there are packages that provide those, I’ve decided to stay close to the default here

So let’s make the test pass:

package set
 
type set struct {
}
 
func NewSet() *set {
  return new(set)
}
 
func (s *set) IsEmpty() bool {
  return true
}

The cool thing about the Eclipse plugin is that it automatically runs the tests whenever you save a file, much like InfiniTest for Java. This is really nice when you’re doing Test-Driven Development.

Now this isn’t much of a test, of course, since it only tests one side of the IsEmpty() coin. Which is what allows us to fake the implementation. So let’s fix the test:

func TestEmpty(t *testing.T) {
  empty := NewSet()
  one := NewSet()
  one.Add("A")
  if !empty.IsEmpty() {
    t.Errorf("Set without elements should be empty")
  }
  if one.IsEmpty() {
    t.Errorf("Set with one element should not be empty")
  }
}

Which we can easily make pass:

type set struct {
  empty bool
}
 
func NewSet() *set {
  s := new(set)
  s.empty = true
  return s
}
 
func (s *set) IsEmpty() bool {
  return s.empty
}
 
func (s *set) Add(item string) {
  s.empty = false
}

Note that I’ve used the string type as the argument to Add(). We’d obviously want something more generic, but there is no Object in Go as there is in Java. I’ll revisit this decision later.

The next test verifies the number of items in the set:

func TestSize(t *testing.T) {
  empty := NewSet()
  one := NewSet()
  one.Add("A")
  if empty.Size() != 0 {
    t.Errorf("Set without elements should have size 0")
  }
  if one.Size() != 1 {
    t.Errorf("Set with one element should have size 1")
  }
}

Which we make pass by generalizing empty to size:

type set struct {
  size int
}
 
func NewSet() *set {
  s := new(set)
  s.size = 0
  return s
}
 
func (s *set) IsEmpty() bool {
  return s.Size() == 0
}
 
func (s *set) Add(item string) {
  s.size++
}
 
func (s *set) Size() int {
  return s.size
}

Now that the tests pass, we need to clean them up a bit:

var empty *set
var one *set
 
func setUp() {
  empty = NewSet()
  one = NewSet()
  one.Add("A")
}
 
func TestEmpty(t *testing.T) {
  setUp()
  if !empty.IsEmpty() {
    t.Errorf("Set without elements should be empty")
  }
  if one.IsEmpty() {
    t.Errorf("Set with one element should not be empty")
  }
}
 
func TestSize(t *testing.T) {
  setUp()
  if empty.Size() != 0 {
    t.Errorf("Set without elements should have size 0")
  }
  if one.Size() != 1 {
    t.Errorf("Set with one element should have size 1")
  }
}

Note again the lack of test infrastructure support compared to, say, JUnit. We have to manually call the setUp() function.

With the code in better shape, let’s add the next test:

func TestContains(t *testing.T) {
  setUp()
  if empty.Contains("A") {
    t.Errorf("Empty set should not contain element")
  }
  if !one.Contains("A") {
    t.Errorf("Set should contain added element")
  }
}

To make this pass, we have to actually store the items in the set, which we do using arrays and slices:

type set struct {
  items []string
}
 
func NewSet() *set {
  s := new(set)
  s.items = make([]string, 0, 10)
  return s
}
 
func (s *set) Add(item string) {
  s.items = append(s.items, item)
}
 
func (s *set) Size() int {
  return len(s.items)
}
 
func (s *set) Contains(item string) bool {
  for _, value := range s.items {
    if (value == item) {
      return true
    }
  }
  return false
}

A slice is a conventient array-like data structure that is backed by a real aray. Arrays can’t change size, but they can be bigger than the slices that they back. This keeps appending items to a slice efficient.

The for loop is the only looping construct in Go, but it’s quite a bit more powerful than the for of most other languages. It gives both the index and the value, the first of which we ignore using the underscore (_). It loops over all the items in the slice using the range keyword.

So now we have a collection of sorts, but not quite yet a set:

func TestIgnoresDuplicates(t *testing.T) {
  setUp()
  one.Add("A")
  if one.Size() != 1 {
    t.Errorf("Set should ignore adding an existing element")
  }
}
func (s *set) Add(item string) {
  if !s.Contains(item) {
    s.items = append(s.items, item)
  }
}

All we have left to make this a fully functional set, is to allow removal of items:

func TestRemove(t *testing.T) {
  setUp()
  one.Remove("A")
 
  if one.Contains("A") {
    t.Errorf("Set still contains element after removing it")
  }
}
func (s *set) Remove(item string) {
  for index, value := range s.items {
    if value == item {
      s.items[index] = s.items[s.Size() - 1]
      s.items = s.items[0:s.Size() - 1]
    }
  }
}

Here we see the full form of the for loop, with both the index and the value. This loop is very similar to the one in Contains(), so we can extract a method to get rid of the duplication:

func (s *set) Contains(item string) bool {
  return s.indexOf(item) >= 0
}
 
func (s *set) indexOf(item string) int {
  for index, value := range s.items {
    if value == item {
      return index
    }
  }
  return -1
}
 
func (s *set) Remove(item string) {
  index := s.indexOf(item)
  if index >= 0 {
    s.items[index] = s.items[s.Size()-1]
    s.items = s.items[0 : s.Size()-1]
  }
}

Note the lower case starting letter on indexOf() that makes it a private method. Since our set is unordered, it wouldn’t make sense to expose this functionality.

Finally, we need to generalize the set so that it can contain any type of items:

func TestNonStrings(t *testing.T) {
  set := NewSet()
 
  set.Add(1)
  if !set.Contains(1) {
    t.Errorf("Set does not contain added integer")
  }
 
  set.Remove(1)
  if set.Contains(1) {
    t.Errorf("Set still contains removed integer")
  }
}

Some digging reveals that we can mimic Java’s Object in Go with an empty interface:

type set struct {
  items []interface{}
}
 
func NewSet() *set {
  s := new(set)
  s.items = make([]interface{}, 0, 10)
  return s
}
 
func (s *set) IsEmpty() bool {
  return s.Size() == 0
}
 
func (s *set) Add(item interface{}) {
  if !s.Contains(item) {
    s.items = append(s.items, item)
  }
}
 
func (s *set) Size() int {
  return len(s.items)
}
 
func (s *set) Contains(item interface{}) bool {
  return s.indexOf(item) >= 0
}
 
func (s *set) indexOf(item interface{}) int {
  for index, value := range s.items {
    if value == item {
      return index
    }
  }
  return -1
}
 
func (s *set) Remove(item interface{}) {
  index := s.indexOf(item)
  if index >= 0 {
    s.items[index] = s.items[s.Size()-1]
    s.items = s.items[0 : s.Size()-1]
  }
}

All in all I found working in Go quite pleasurable. The language is simple yet powerful. The go fmt kills discussions about code layout, as does the compiler’s insistence on braces with if. Where Go really shines is in concurrent programming, but that’s something for another time.

What do you think? Do you like this opiniated little language? Do you use it at all? Please leave a word in the comments.

Reference: First Steps Into the World of Go from our JCG partner Remon Sinnema at the Secure Software Development blog.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Chri
Chri
8 years ago

I liked Go first, but then I was more and more annoyed be the inconveniences like missing generics, missing functional programming idioms, missing exception handling, mediocre API docs, lousy tool support and so on.

Back to top button