Engine Yard Blog RSS Feed

Google recently publicly released their new programming language, Go. I've known about this for some time, having worked for the big G while it was in development, although not directly involved.

Google has plenty of special purpose languages, but this is the first general purpose language to come out of Mountain View. That fact alone makes it quite interesting. Add to it that some of the original C and UNIX people are involved, and it becomes something that requires investigation.

What's it for?

Go is a systems programming language. Think of it as a modern C. It's meant for the same types of programming: operating systems, compilers, infrastructure pieces.

What's it all about?

To summarize the main golang.org page, Go is: simple, fast, safe, concurrent, open source, and fun.

Simple Go is designed to be easy to work with, requiring minimal overhead and boilerplate. This is reminiscent of dynamic languages such as Python and Ruby.
Fast Go's syntax is designed well, being regular and relatively simple. In addition to its dependency management features, this leads to fast compile times. Go provides the runtime efficiency of a statically typed, compiled language such as C or Java without the bloated syntax of those languages.
Safe Go also provides the type safety of a statically typed, compiled language, without the syntax bloat usually associated with a statically typed language. In addition to type safety, Go is memory safe: you should never get a null pointer error when you use an invalid memory address. Additionally, it is a garbage collected language, which eliminates even more overhead code and memory related errors.
Concurrent Go is modern language, designed for use in multi-core environments. Concurrency is built into the language, as is inter process communication (via channels).
Open Source Go is released under a BSD style license.
Fun You'll have to decide that for yourself... but we're enjoying it here at least.

What's so different about it?

Go is an object-oriented language, but not in the style that most programmers these days think of OO. Go doesn't have implementation inheritance (more on that later). Also, you don't define methods inside a class as in many OO languages. Instead, you add methods to types in a way somewhat reminiscent of CLOS . For example:


func (file *File) Read(b []byte) (number_bytes_read int, err os.Error) { ... }
  if file == nil {
    return -1, os.EINVAL
  }
  r, e := syscall.Read(file.fd, b);
  if e != 0 {
    err = os.Errno(e);
  }
  return int(r), err
}

Here we have a function Read that's being attached to the type *File. Read takes a single parameter, b, which is an array of byte and it returns an int named number_bytes_read and an Error named err. There are three things to note here. Read, is (1) being attached to an existing type (a pointer type at that), and (2) returns multiple values which (3) are named. Even if the return value names aren't used, they are superb for documenting what the returned values mean.

Points of Note

Fast compilation

Go compiles fast. Really fast. This means no long waits for builds. More importantly, it means a tighter test-code development cycle.

Less typing... and less typing

Here's the basic way to declare a variable (a string in this case):

var s string = "";

If the type can unambiguously be inferred, you can do:

var s = "";

and using the initializing declaration operator, you can simplify even more:

s := "";

I say shiny: this is pretty sweet. Minimal boilerplate and type inference.

Arrays and Slices

Arrays are, of course, built-in to the syntax. Arrays are fixed size linear collections of data. They work pretty much as expected except that they don't support the pointer arithmetic tricks that C programmers take for granted. So, you get an actual pointer to an array, not just a pointer to something that happens to be the first thing in the array.

Slices are the idiomatic way to work with sequences of data in Go. They wrap arrays to give a more powerful way to work. Slices are too big of a topic to explore in this post, but basically they provide a dynamic window onto an array.

Arrays have a size as an integral aspect of the type, thus \[3\]int is a different type than \[5\]int. Slices have a size as part of their data.

You create a slice of an array by specifying the initial and one past the final index. For example:


a :=  \[4\]int{1, 2, 3, 4};
s := a[1:3];

s \[0\] == 2
s \[1\] == 3

Here s has a size of 2 (3 - 1) and type []int.

Here's a function that takes a slice as its parameter:


func F(buf []byte)

You can tell this is a slice because there is no size inside the brackets.You could call the above function with an explicit slice:


var a  \[5\]byte;
F(a[2:4]);

but you can also implicitly pass a slice that covers a complete array:


var a  \[5\]byte;
F(a);

Strings and Maps are built-in

Something that's nice to see is that strings are built into the language and standard library as in C (but unlike C++). The proliferation of string libraries in C++ caused great confusion and frustration in C++'s early days.

What's even nicer to see is that hashes are built-in. Having the basic and extremely useful types built-in to the language make it possibly to optimize them quite aggressively. This way. programmers don't have to worry about choosing a slow string or hash library.

Maps are very flexible, here's an example from Effective Go:


var timeZone = map \[string\] int {
  "UTC":  0*60*60,
  "EST": -5*60*60,
  "CST": -6*60*60,
  "MST": -7*60*60,
  "PST": -8*60*60,
}

func offset(tz string) int {
  if seconds, ok := timeZone \[tz\]; ok {
    return seconds
  }
  log.Stderr("unknown time zone", tz);
  return 0;
}

Note the multiple return values where the second is an indicator as to whether a valid value was found.

Interfaces

Interfaces are just a collection of function signatures. Here's an example of interface use taken from the IO library:


type Reader interface {
  Read(p []byte) (n int, err os.Error);>
}

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

This is nothing too earth shattering if you're familiar with Java. If you're used to Ruby, this is something a little different: actually having to codify the protocols to which your objects conform (see Jim Weirich's RubyConf 2009 talk for a discussion of this).

Making the connection between interfaces and the types that implement them is just the opposite. It's implicit—as it is in Ruby et.al.— not explicit, as it is in Java. For example here is something that can be used whenever a Reader is expected:


type limitedReader struct {
  r Reader;
  n int64;
}

func (l *limitedReader) Read(p []byte) (n int, err os.Error) { ... }

This example also shows declaring a variable (a struct member in this case) of an interface type.

One nice thing about the way interfaces are done in Go is that if you define an interface that declares some methods of an existing type, you have implicitly performed an Extract Interface refactoring. None of the types implementing those methods have to be touched: they can now be implicitly and automatically used wherever you use the new interface.

One nice feature is the ability to make composite interfaces:


(type ReadSeeker interface {
  Reader;
  Seeker;
}
)

Here, the ReadSeeker interface encompasses everything in both the Reader and Seeker interfaces. Anything that implements the methods declared in Reader and Seeker can be used wherever Reader, Seeker, or ReadSeeker is used.

No Implementation Inheritance

It is the lack of implementation inheritance that seems odd. Most of us are used to considering it as a necessary part of an OO language. In his 1996 talk "What is Object-Oriented Programming?" Bjarne Stroustrup (creator of C++) went so far as to say:

_Consider a language having some form of method lookup without having an inheritance mechanism. Could that language be said to support object-oriented programming? I think not._

It's interesting to note, however, that Alan Kay (arguably the father and namer of OO) didn't originally consider inheritance necessary as part of the language (in "The Early History of Smalltalk"):

_I just decided to leave inheritance out as a feature in Smalltalk-72, knowing that we could simulate it back using Smalltalk's LISPlike flexibility. The biggest contributor to these AI ideas was Larry Tesler who used what is now called "slot inheritance" extensively in his various versions of early desktop publishing systems. Nowadays, this would be called a "delegation-style" inheritance scheme._

Inheritance was jumped on when OO hit the scene back in the 80s. We saw it used far too much and too freely. Over the years the thinking shifted to a more sensible approach of composition rather than inheritance. This is what Kay was referring to back in '72.

Garbage Collection

Go has a rudimentary mark-sweep garbage collector, but there are plans to improve that. One of the boons of having garbage collection is that you completely sidestep entire classes of memory related bugs. It also means you don't have to write the code to deal with memory management. In the context of Go being a language that embraces parallelism, garbage collection means that you can avoid worrying about cleaning up allocated memory.

Concurrency

In today's world of multi-core processors, concurrent programming is becoming increasingly relevant. The Go authors recognized this and designed Go for use in writing concurrent programs. Go embodies a different philosophy of concurrency than most systems languages:

_Do not communicate by sharing memory; instead, share memory by communicating._

Go offers two primitives to accomplish this sharing by communication: goroutines and channels.

Goroutines

Goroutines are functions executing in parallel with other goroutines. The Go runtime manages parallelizing all of the goroutines using operating system primitives. They are light-weight and easy to use.


go doSomething(); // Do something, but do not wait for it to complete

You can even pass an anonymous function literal:


(go func() {
  time.Sleep(5);
  fmt.Prinln("Hello World");
}();
)

Channels

With goroutines, executing functions in parallel is easy. However we still need a mechanism for communicating between them. That's what channels are for. Channels provide a synchronized, type-safe way to communicate values from one goroutine to another.


ch := make(chan int); // unbuffered channel of integers

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

/* Do something else for a while */

sum := <-ch;
fmt.Println(sum);

The above example shows how channels can be used to communicate between two goroutines, and it also shows how they can be synchronized. The binary send operation will block until another goroutine receives the value, and the unary receive operation will block until another goroutine sends a value. So a channel can be used as a simple way to synchronize a goroutine.


ch := make(chan bool);
go func() {
  /* do some stuff */
  ch <- true;
}();
<-ch; // strobe the channel to synchronize

What's not there?

Some things are absent from Go (at least for now), including generics, exceptions, and assertions. Generics and exceptions might be added in the future, when an acceptably clean and efficient design is devised. Keep in mind that Go is meant for system programming. Efficiency is important, and language features such as generics and exceptions have non-trivial runtime performance implications. Go is not a kitchen sink language—it is designed for a fairly specific use.

Conclusions

Go is extremely easy to dive into. There are a minimal number of fundamental language concepts and the syntax is clean and designed to be clear and unambiguous.

Go is still experimental and still a little rough around the edges. For example, if a function has a return value, and it returns from every branch of execution, it will still complain if it doesn't return from the end of the function.

That said, Go allows you to do Pascal style return values where you give the result a name, assign to it, and use it in computation. However, you still have to put the return keyword at the end.

The only typing is duck typing. To Rubyists, this is a great thing.

As Carl Lerche comments, "It's not production quality yet, but there's enough that it's interesting to explore."


This post was co-authored by Sam Tesla.


Tagged:

comments powered by Disqus