BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Google Go: A Primer

Google Go: A Primer

What is Go?

Google recently announced their new programming language, Go. It is designed to bring some of the advances of modern programming languages back down to the systems arena where C still dominates today. However, the language is still experimental and evolving.

The Go authors set out to design a language that would be simple, fast, safe and concurrent. The language is simple enough that it does not even require a symbol table to parse. It compiles very quickly; sub-second compile times for entire projects are normal. It is garbage-collected, so it is safe with regards to memory. It is statically type-checked and does not allow type coercion, so is safe with regards to types. It also offers a powerful mechanism for implementing concurrency that is built right into the language.

Reading Go

Go's syntax continues in the same vein as C's. Programs are organized as functions whose bodies are a sequence of statements. Blocks of code are surrounded by curly-braces. The language has a limited set of reserved keywords. Expressions use the same infix operators. There are very few syntactical surprises.

The Go authors have adhered to a single guiding principle when designing the language: simplicity and clarity above all else. Several of the new syntactical constructs provide concise means to express common idioms that are more verbose in C. Others address language choices that, after several decades of use, have been shown to be unwise.

Variable Declarations

Variables are declared as follows:

var sum int // Just a declaration
var total int = 42 // A declaration with initialization

Most notably, the type in these declarations comes after the variable name. This may seem strange at first, but has some benefits on clarity. For example, take the following snippet of C:

int* a, b;

It is not apparent, but that actually means that a is a pointer and b is not. In order to declare them both pointers, the asterisk must be repeated. However in Go, they can both be declared pointers as follows:

var a, b *int

If a variable is being initialized, the compiler can typically infer its type, so it is not necessary for the programmer to type it out:

var label = "name"

However, at that point, the var keyword is almost superfluous. So, the Go authors introduced a new assignment operator which both declares and initializes a new variable:

name := "Samuel"

Conditionals

Conditionals in Go have the same familiar if-else construction as those in C, but the condition need not be wrapped in parentheses. This creates less visual clutter when reading code.

Parentheses are not the only clutter that have been removed. A simple statement can be included before the condition so that the following code:

result := someFunc();
if result > 0 {
	/* Do something */
} else {
	/* Handle error */
}

Can be reduced to this:

if result := someFunc(); result > 0 { 
	/* Do something */
} else {
	/* Handle error */
}

However, in the latter example, result is only in scope inside the conditional block -- in the former, it is available in the containing scope.

Switches

Switches once again are similar, but improved. Like conditionals, they allow a simple statement to proceed the expression that is being switched upon. However, they deviate even further from their C counterparts.

First, to make switches more concise, two changes were made. Cases can be comma-separated lists of values, and fall-through is no longer the default behavior.

So, the following C code:

int result;
switch (byte) {
 case 'a':
 case 'b':
   {
     result = 1
     break
   }

 default:
   result = 0
}

Becomes this in Go:

var result int
switch byte {
case 'a', 'b':
  result = 1
default:
  result = 0
}

Second, Go switches can match on much more than integers and characters. Any valid expression can be the value of a case statement, so long as it is the same type as the switch expression.

So, the following C code:

int result = calculate();
if (result < 0) {
  /* negative */
} else if (result > 0) {
  /* positive */
} else {
  /* zero */
}

Becomes this in Go:

switch result := calculate(); true {
case result < 0:
  /* negative */
case result > 0:
  /* positive */
default:
  /* zero */
}

That idiom is so common, that if the switch value is omitted, it is assumed to be true. So the above could be rewritten as:

switch result := calculate(); {
case result < 0:
  /* negative */
case result > 0:
  /* positive */
default:
  /* zero */
}

Loops

Go has only one keyword to introduce a loop. However, it offers all of the looping behaviors available in C except the do-while.

Condition

for a > b { /* ... */ }

Initializer, Condition and Step

for i := 0; i < 10; i++ { /* ... */ }

Range

The right-hand expression of a range clause must be an array, slice, string or map, or a pointer to an array; or it may be a channel.

for i := range "hello" { /* ... */ }

Infinite

for { /* ever */ }

Functions

The syntax for declaring functions differs from C. As with variable declarations, types are declared after the terms they describe. So the following function in C:

int add(int a, b) { return a + b }

Becomes this function in Go:

func add(a, b int) int { return a + b }

Multiple Return Values

A common idiom in C is to either reserve a return value to indicate error (e.g. read() returning 0), or to reserve the return value to communicate status and pass in a pointer to a memory location in which to store results. This encourages unsafe programming practices and is not viable in a managed language such as Go.

Recognizing that this problem extends beyond the simple need to communicate both function results and errors, the Go authors built in the capability to return multiple values from a function.

As an example, this is a function to return both parts of an integer division:

func divide(a, b int) (int, int) {
  quotient := a / b
  remainder := a % b
  return quotient, remainder
}

With multiple return values, it is good to have the code document which is which - Go allows you to give the return values names, just like parameters. You can then assign values to those return variables just like any other. So we could rewrite divide like this:

func divide(a, b int) (quotient, remainder int) {
  quotient = a / b
  remainder = a % b
  return
}

The presence of multiple-return values has given rise to the "comma-ok" pattern. Functions which can fail can return a second, boolean, result to indicate success. Alternatively, they can return an error object. So it's not uncommon to see code like this:

if result, ok := moreMagic(); ok {
  /* Do something with result */
}

Anonymous Functions

Having a garbage collector opens the door for a wide variety of features - among those are anonymous functions. Go provides a simple syntax for declaring anonymous functions. These functions create a lexical closure on the scope in which they were defined like blocks in many dynamic languages.

Consider the following program:

func makeAdder(x int) (func(int) int) {
  return func(y int) int { return x + y }
}

func main() {
  add5 := makeAdder(5)
  add36 := makeAdder(36)
  fmt.Println("The answer:", add5(add36(1))) //=> The answer: 42
}

Primitive Types

Like C, Go offers a handful of primitive types. The usual boolean, integer and floating-point types are available. It has a Unicode string type and an array type. The language also introduces two new types: slice and map.

Arrays & Slices

Arrays in Go are not dynamic like they are in C. Their size is part of their type, and is determined at compile time. To index into an array the familiar C syntax is used (e.g. a[i]), and as with C an array index is 0-based. The compiler provides a built-in function, which is evaluated at compile time, to determine the length of an array (e.g. len(a)). If a write to an index beyond the bounds of an array is attempted, a run-time error will be generated.

Go also offers slices, which put a new twist on arrays. A slice represents a contiguous segment of an array, allowing a programmer to refer to specific sections of the underlying storage. The syntax for constructing a slice is similar to the syntax for accessing an array element:

/* Construct a slice on ary that starts at s and is len elements long */
s1 := ary[s:len]

/* Omit the length to create a slice to the end of ary */
s2 := ary[s:]

/* Slices behave just like arrays */
s[0] == ary[s] //=> true

// Changing the value in a slice changes it in the array
ary[s] = 1
s[0] = 42
ary[s] == 42 //=> true

The segment of the array that the slice references can be changed by assigning a new slice to the same variable:

/* Move the start of the slice forward by one, but do not move the end */
s2 = s2[1:]

/* Slices can only move forward */
s2 = s2[-1:] // this is a compile error

The length of a slice can be changed so long as it does not exceed the slice's capacity. The capacity of a slice s is the size of the array from s[0] to the end of the array, and is returned by the cap() built-in function. A slice's length can never exceed its capacity.

Here is an example that shows how length and capacity interact:

a := [...]int{1,2,3,4,5} // The ... means "whatever length the initializer has"
len(a) //=> 5

/* Slice from the middle */
s := a[2:4] //=> [3 4]
len(s), cap(s) //=> 2, 3

/* Grow the slice */
s = s[0:3] //=> [3 4 5]
len(s), cap(s) //=> 3, 3

/* Cannot grow it past its capacity */
s = s[0:4] // this is a compile error

Often, a slice is all that is needed for a program. In that case, a programmer need not have an array at all. Go offers two ways to make slices directly without ever referencing the underlying storage:

/* literal */
s1 := []int{1,2,3,4,5}

/* empty (all zero values) */
s2 := make([]int, 10) // cap(s2) == len(s2) == 10

Maps

One data type that is present in almost every dynamic language that is popular today, but that is missing from C, is a dictionary. Go offers a primitive dictionary type called a map. The following example shows how to make and use a Go map:

m := make(map[string] int) // A mapping of strings to ints

/* Store some values */
m["foo"] = 42
m["bar"] = 30

/* Read, and exit program with a runtime error if key is not present. */
x := m["foo"]

/* Read, with comma-ok check; ok will be false if key was not present. */
x, ok := m["bar"]

/* Check for presence of key, _ means "I don't care about this value." */
_, ok := m["baz"] // ok == false

/* Assign zero as a valid value */
m["foo"] = 0;
_, ok := m["foo"] // ok == true

/* Delete a key */
m["bar"] = 0, false
_, ok := m["bar"] // ok == false

Object Orientation

The Go language supports a style of object-oriented programming similar to that used in C. Data is grouped together into structs, and then functions are defined which operate on those structs. Similar to Python, the language offers a way to define the functions and then call them so that the syntax is not cumbersome.

Structs

Declaring a new struct type is simple:

type Point struct {
  x, y float64
}

Values of this type can now be allocated using the built-in function new, which returns a pointer to the value in memory with all slots initialized to the zero value.

var p *Point = new(Point)
p.x = 3
p.y = 4

That can get verbose, and one of the goals of the Go language is to be concise whenever possible. So a syntax is provided that both allocates and initializes the struct at the same time:

var p1 Point = Point{3,4}  // Value
var p2 *Point = &Point{3,4} // Pointer

Methods

Once a type has been declared, functions can be declared which take that type as an implicit first parameter:

func (self Point) Length() float {
  return math.Sqrt(self.x*self.x + self.y*self.y);
}

Those functions can then be called as methods on the struct:

p := Point{3,4}
d := p.Length() //=> 5

Methods can actually be declared on both value and pointer types. Go will handle referencing or dereferencing objects as appropriate, so it is possible to declare methods on both type T and type *T and have them be used as appropriate.

Let us extend our Point class with a mutator:

/* Note the receiver is *Point */
func (self *Point) Scale(factor float64) {
  self.x = self.x * factor
  self.y = self.y * factor
}

Then we can call it like this:

p.Scale(2);
d = p.Length() //=> 10

It is important to understand that the self that is passed in to MoveToXY is a parameter like any other, and parameters are passed by value, not by reference. That is why it must be declared as a pointer type in order to actually change the value. If it were declared as just Point, then the struct that was modified inside the method would not be the same one at the call site - values are copied when they are passed to a function, they are also discarded at the end of it.

Interfaces

Dynamic languages such as Ruby emphasize a style of object-oriented programming that places more importance on what behavior an object has rather than what type that object is (duck typing). One of the most powerful features that Go brings with it is the ability to program with that duck-typed mentality, and check for adherence to those defined behaviors at compile time. The name given to the behaviors is interfaces.

Defining an interface is simple:

type Writer interface {
  Write(p []byte) (n int, err os.Error)
}

That defines an interface with a method for writing a buffer of bytes. Any object which implements that method also implements the interface. No declarations are required as in Java, the compiler just figures it out. This gives the expressiveness of duck-typing with the safety of static type-checking.

The way interfaces behave in Go allows developers to discover their programs' types as they write them. If there are several objects that all have the behavior, and a developer wishes to abstract on that behavior, they can create an interface and then use that.

Consider the following code:

// Somewhere in some code:
type Widget struct {}
func (Widget) Frob() { /* do something */ }

// Somewhere else in the code:
type Sprocket struct {}
func (Sprocket) Frob() { /* do something else */ }

/* New code, and we want to take both Widgets and Sprockets and Frob them */
type Frobber interface {
  Frob()
}

func frobtastic(f Frobber) { f.Frob() }

It is important to note that every object implements the empty interface:

interface {}

Inheritance

The Go language does not have inheritance, at least not the way most languages do. There is no hierarchy of types. Go encourages the use of composition and delegation over inheritance, and offers some syntactic sugar to make it more bearable.

Given these definitions:

type Engine interface {
  Start()
  Stop()
}

type Car struct {
  Engine
}

I can then write the following:

func GoToWorkIn(c Car) {
  /* get in car */

  c.Start();

  /* drive to work */

  c.Stop();

  /* get out of car */
}

When I declared the Car struct, I gave it what is called an anonymous member. That is a member which is identified only by its type. The anonymous member is a member like any other, with a name the same as the type. So I could have also written c.Engine.Start(). The compiler automatically delegates calls made on Car to methods on its Engine if the Car does not have methods of its own to satisfy them.

The rules for resolving methods provided by anonymous members are conservative. If a method is defined for a type, it is used. If not, and a method is defined for an anonymous member that is used. If there are two anonymous members that both provide a method, the compiler will produce an error, but only if that method is called.

This composition is achieved via delegation, not inheritance. Once the anonymous member's method has been called, flow has been delegated to that method entirely. So you cannot simulate type hierarchy like this:

type Base struct {}
func (Base) Magic() { fmt.Print("base magic") }
func (self Base) MoreMagic() { 
  self.Magic()
  self.Magic()
}

type Foo struct {
  Base
}
func (Foo) Magic() { fmt.Print("foo magic") }

When you create a Foo object, it will respond to both methods that Base does. However, when you call MoreMagic you will not get the results you expect:

f := new(Foo)
f.Magic() //=> foo magic
f.MoreMagic() //=> base magic base magic

Concurrency

The Go authors chose a message-passing model as their recommended method for concurrent programming. The language does still support shared memory, however the authors have the following philosophy:

Do not communicate by sharing memory; instead, share memory by communicating.

The language offers two basic constructs to achieve this paradigm: goroutines and channels.

Goroutines

Goroutines are lightweight parallel paths of program execution similar to threads, coroutines, or processes. However, they are sufficiently different from each that the Go authors elected to give them a new name and discard any connotative baggage that the other terms might have.

Spawning a goroutine to run a function named DoThis is as simple as this:

go DoThis() // but do not wait for it to complete

Anonymous functions can also be used:

go func() {
  for { /* do something forever */ }
}() // Note that the function must be invoked

These goroutines are mapped to the appropriate operating-system concurrency primitives (e.g. POSIX threads) by the Go runtime.

Channels

With goroutines, parallel execution of code is easy. However, a mechanism for communicating between them is still needed. Channels provide a FIFO communication queue that can be used for just this purpose.

Here is the syntax for working with channels:

/* Creating a channel uses make(), not new - it was also used for map creation */
ch := make(chan int)

/* Sending a value blocks until the value is read */
ch <- 4

/* Reading a value blocks until a value is available */
i := <-ch

For example, if we wanted to do some long-running numerical computation we could do this:

ch := make(chan int)

go func() {
  result := 0
  for i := 0; i < 100000000; i++ {
    result = result + i
  }
  ch <- result
}()

/* Do something for a while */

sum := <-ch // This will block if the calculation is not done yet
fmt.Println("The sum is:", sum)

The blocking behavior of channels is not always the best. The language offers two ways to customize this:

  1. A programmer can specify a buffer size - sending to a buffered channel will not block unless the buffer is full, and reading from a buffered channel will not block unless the buffer is empty
  2. The language also offers the ability to send and receive without ever blocking, while still reporting if the operation succeeded
/* Create a channel with buffer size 5 */
ch := make(chan int, 5)

/* Send without blocking, ok will be true if value was buffered */
ok := ch <- 42

/* Read without blocking, ok will be true if a value was read */
val, ok := <-ch

Packages

Go offers a simple mechanism for organizing code: packages. Each file begins with a simple declaration of what package it belongs to, and each file can import the packages it uses. Any names which begin with a capital letter are exported from a package, and are available to be used by other packages.

Here is a complete source file:

package geometry

import "math"

/* Point is capitalized, so it is visible outside the package. */

type Point struct {

  /* the fields are not capitalized, so they are not visible
     outside of the package */

  x, y float64 
}

/* These functions are visible outside of the package */

func (self Point) Length() float64 {
  /* This uses a function in the math package */
  return math.Sqrt(self.x*self.x + self.y*self.y)
}

func (self *Point) Scale(factor float64) {
  self.setX(self.x * factor)
  self.setY(self.y * factor)
}

/* These functions are not visible outside of the package, but can be
   used inside the package */

func (self *Point) setX(x float64) { self.x = x }
func (self *Point) setY(y float64) { self.y = y }

What's Missing

The Go authors have tried to let clarity of code guide all of their decisions regarding the design of the language. A secondary mission has been to produce a language which compiles quickly. With these two criteria to steer them, many features from other languages have not made it in. Many programmers will find that their favorite language feature is not present and indeed some may feel that the language is not yet usable for lack of certain features common in other languages.

Two such missing features are exceptions and generics, both very helpful in other languages. Neither feature is currently part of Go. But since the language is still experimental, these may make it in there eventually. However, when comparing Go to other languages, we should remember that Go is intended to be a replacement for C in systems programming. And in this light, the various missing features don't look like such a drawback.

Finally, since the language has just been released, it does not have much in the way of libraries or tools: there are no IDEs for Go. The standard library has useful code, but it is small compared to what is available in more established languages.

Rate this Article

Adoption
Style

BT