home

Introduction To Golang: Structures, Data and Instances

Jan 07, 2013

Go has an elegant type system accompanied by a simple syntax. The first part involves creating a custom structure:

type Unicorn struct {
  Name string
  Age int
  weight int
}

In Go, types (functions, structures, variables, ...) are exported based on the casing of the first letter. In our example not only is Unicorn accessible from outside, but so too are its Name and Age members. weight on the other hand, is not.

When creating an instance of a type, we need to decide if we want a pointer or the actual value. Most of the time you'll probably want a pointer, but it's important to understand the difference. We'll get to that in a second. First, let's look at how we can create either one.

To create a value we use the var keyword with a type:

var larry Unicorn

In the case where the type can be inferred by an assignment, we can leave out the type:

var count = 1
// same as
var count int = 1

The syntax to create a pointer is only slightly different:

var larry *Unicorn
larry = new(Unicorn)

This is quite verbose for code you'll be writing a lot. What you'll actually want to do is use the := operator to define and create your pointer (using this approach also lets you omit var):

larry := new(Unicorn)

There's one final tweak. If you want to create an instance and assign values, you can use yet another syntax

larry := &Unicorn{
  Name: "Larry",
  Age: 240,
}
// same as
larry := new(Unicorn)
larry.Name = "Larry"
larry.Age = 240

To recap, most of the time you'll use:

// create an instance pointer
larry := new(Unicorn)

// create an instance pointer and initialize some members
larry := &Unicorn{
  Member1: value1,
  MemberN: valueN,
}

// create a value type
var vampires = 43

So far we've only dealt with data fields. What about methods? Go lets us attach methods to our structures with a simple syntax:

func (u *Unicorn) Sleep(years int) (ok bool) {

}

The parts of this deceleration are a little different than what you are probably used to. First though, note that functions follow the same casing convention. Therefore, this function will be visible outside the package (in other word, it's public). About the order, the return value is at the end, the arguments in the middle, and the type it's attached to at the start.

It's possible (and common) to declare a function that isn't attached to a specific type or doesn't return a value:

func blink(times int) {

}

Back to our first function. Notice that this behaves on a pointer type (*Unicorn). The function could be attached to a value type:

func (u Unicorn) Sleep(years int) (ok bool) {
}

Similarly, the functions arguments could be either value or pointer types. The benefits and drawbacks of pointers vs values are the same whether we are talking about arguments or the type being attached. These are the same advantages and disadvantages you see in almost every other languages. A change to a pointer made within the function will be visible outside the function. Furthermore, passing pointers around, especially for large types, is more efficient. On the flip side, if you don't want changes made to the instance within the function to change the original, you definitely don't want to pass a pointer:

func (u Unicorn) Rename(name string) {
  u.Name = name
}
....
larry := &Unicorn{
  Name: "Larry",
}
larry.Rename("sally")

// What's larry.Name ?

After calling Rename, larry.Name remains Larry because only a copy of our structure was passed. As is, Rename is useless and should declared as (u *Unicorn)

A final, slightly off topic, note. Earlier we saw that our return value was named. This is optional, but useful. In the above example, the ok variable is available for us to assign to without having to declare it explicitly within the function. On top of that, Go supports multiple return values:

func png(file *os.File) (width int, height int) {
  bytes := make([]byte, 8)
  file.ReadAt(bytes, 16)
  width = int(bytes[0]) << 24 | int(bytes[1]) << 16 | int(bytes[2]) << 8 | int(bytes[3])
  height = int(bytes[4]) << 24 | int(bytes[5]) << 16 | int(bytes[6]) << 8 | int(bytes[7])
  return
}

So there's a lot of options around creating instances. For the most part though, you'll use x := new(Something) or x := &Something{...}...anything else you might use is pretty standard stuff.

More Go goodness to come