Thoughts on the Go Programming Language

Part of the description of Go from its homepage states that it’s a “compiled language that feels like a dynamically typed, interpreted language”. In my relatively brief experience with the language, that is a statement I can attest to. I feel like an expert in Go could build natively-compiled applications at a rate comparable to Python over C++ or Java. Part of this has to do with its extensive library, and partly because the language is just not particularly complicated. Not to mention that the language’s concurrency model is very intuitive.

My initial interest in Go stemmed from goroutines. These implement concurrency in an extremely simple and highly-abstracted fashion: concurrently-executing functions. There isn’t much more to it to than that; goroutines are apparently multiplexed over a pool of host threads which are hidden from the programmer. You can let them spin off on their own and terminate, or  synchronize and communicate with them using message-passing in the form of channels. In this sense, concurrency in Go feels a lot like Erlang. Some other cool features include closures and array slices. Technically Go isn’t object-oriented, but you’re able mimic it by using interfaces and embedding methods into structures. Also, Go is garbage-collected.

Actually, the features of Go make it feel like a combination of a bunch of different languages. There’s obvious inspiration from C, and some from what feels like Java (interfaces, GC), Erlang (message-passing and concurrency), functional languages in general (closures) and Python. Lets dive into an example which illustrates a few of the things I’ve mentioned so far.

Here’s the (highly synthetic) situation: We’d like to compute the square roots of a list of floating-point numbers in parallel using our own hand-rolled function implementing Newton’s Method. We’re going to compute each square root in a separate goroutine and collect each value through a separate channel to that goroutine. To make things more interesting, we’ll package a bunch of it up using a closure.

/* Compute and return the square root of x. Note
 * that return type comes after function parameters */
func newtons_method (x float64) float64 {

	/* No parentheses around conditional */
	if x == 0.0 {
		panic ("Divide by zero!") // throw exception
	}

	/* "Initialize" statement - variable type inferred by
 	 * compiler */
	last := float64(0.0)
	y := float64(x / 2.0)

	/* No "while" loops - iterate "for as long as" condition */
	for math.Fabs(last - y) > 0.0001 {
		last = y
		y = y - ((y * y - x) / (2 * y))
	}

	return y
}

/* Given a slice of floats, compute their square roots in
 * parallel and print the results. Note variable name
 * precedes the type. */
func compute_sqrts (L []float64) {

	/* Type defined at compile-time */
	n := len (L)

	/* A "slice" definition - used here as expandable array */
	var chans []chan float64

	/* Fill the slice with channels */
	for i := 0; i < n; i++ {
		chans = append(chans, make(chan float64))
	}

	/* For index, element in the list */
	for i, x := range L {

		/* Copy the range variables so the closures
		 * reference the right values */
		__x := x
		__i := i

		/* Define a quick closure and execute it in parallel
		 * as a goroutine via the go () statement */
		go func() {
			/* Calculate square root */
			var root float64 = newtons_method (float64(__x))
			chans[__i] <- root /* Write result to channel */
			close(chans[__i])  /* Close the channel */
		} ()
	}

	for i := range chans {
		x := <- chans[i] /* Read from channel */
		fmt.Println ("Square root of ", L[i], " is ", x)
	}
}

Things to note:

  1. Yes, there are like ten different ways to declare variables
  2. Yes, there are like ten different ways to declare for loop conditions (and no while loops)
  3. Yes, opening braces (‘{‘) must go on the same line as function definition or condition
  4. There is a new allocation directive called make(…) in addition to new(…)
  5. This program is primarily imperative
  6. Even though there is flexibility in how you define variables, they are still very much strongly typed

Obviously, the most interesting part of this little program is what’s going on in lines [50-55]. This is a function literal and in Go, all function literals are also closures; meaning, variables referenced by the function literal are in-scope for as long as necessary for the literal to complete. In addition, this function literal is preceded and followed by go and (). This executes the literal inside a goroutine, which as discussed earlier, will run concurrently with the main thread and all other goroutines (within reason).

The literal itself calls the newtons_method() function, and writes the return value into the closure’s channel. It then closes the channel, which would normally indicate that the closure has completed its work and executed. The values written to each of the respective channels will wait to be read, even after the goroutine has exited.

I mentioned earlier that you can play around with some pseudo-OO in Go, so I’ll give a brief example:

/* Interface definition - any structure which
 * implements newtons_method (x float64)
 * also implements this interface */
type SqrtInterface interface {
	newtons_method ()
}

/* Structure definition - this is also
 * a SqrtInterface type */
type SqrtPair struct {
	x float64
	sqrt_x float64
}

/* Overloaded newtons_method() which is bound
 * to pointers of type SqrtPair. Will reference
 * the members of the 'calling' struct. */
func (sp *SqrtPair) newtons_method () {

	/* ... */

	sp.sqrt_x = newtons_method (sp.x)
}

/* This does nothing except call member functions
 * of SqrtInterface interface objects */
func compute_for_pair (sp SqrtInterface) {
	sp.newtons_method ()
}

func main () {

	/* Note sp has type *SqrtPair */
	sp := new (SqrtPair)
	sp.x = 17
	compute_for_pair (sp)
	fmt.Println ("Square root of ", sp.x, " is ", sp.sqrt_x)
}

I hope that blows your mind, because it blew mine. Let me illustrate what’s going on:

  • Define an interface of type SqrtInterface which is basically empty except for a member function called newtons_method()
  • Define a structure of type SqrtPair with a couple of floats
  • Embed a function called newtons_method() into structures of type SqrtPair*
  • SqrtPair now implements the interface SqrtInterface
  • SqrtPair.newtons_method() can now be called on instantiated SqrtPair structures acting as SqrtInterface objects which modifies their internal members even though the method wasn’t present in the initial structure definition

So obviously there’s a lot of cool stuff happening inside Go. TIOBE ranks Go as the world’s 21st most popular programming language at the time of this post (interpret these rankings however you wish), beating out D, arguably one of its closest competitors, at 33rd. GitHub has a huge pile of Go projects. There’s no doubt that Go is a very powerful language, which is even supported by a GCC backend in addition to its standard compiler. It also comes with an enormous standard library with built-in items like HTTP servers. I like Go a lot, but there are some nagging (and probably petty) issues I have with it which probably means I won’t be using it as my language of choice in the near future:

  • Although minimalistic, I find the syntax kind of random. In some sense it can beneficial to mix the best aspects of Python and Erlang and C syntax together, I think it makes the end result kind of messy. For example, spliceVar = append(spliceVar, item) vs. mapVar[key] = item
  • I am frustrated that I can define variables ten different ways, including not needing to specify variable type, but I can’t put the type before the variable definition.
  • Similarly, for a language so flexible, I don’t know why it’s required to put braces on opening lines, and wrap one-line loops and if-statements in braces.
  • No templates. How can there be no templates you ask? There just aren’t. I don’t know why.
  • Although technically not object-oriented, you can mix in elements of OO as you see fit. I feel that this can lead to the same mix of paradigms that drives C++ people insane.
  • “Exported” variables and functions are denoted by title-case, getting rid of ‘extern’ and ‘public’. I don’t know what sort of problem this is trying to solve, but I suspect it can just lead to more confusion.

But to end on a positive note, a list of Go features I love playing around with:

  • Goroutines and channels
  • Closures / function literals
  • Huge standard library, including built-in RPC
  • Slices
  • Multiple return values (so good!)
Advertisements

7 responses to “Thoughts on the Go Programming Language

  • Andrew

    In your first example you only need one result channel that you receive from n times.

    Of the “ten different ways to declare variables,” you’ve only shown two. In practice there are only two. You “can’t put the type before the variable definition” because that would be inconsistent with the rest of the language.

    In the example “spliceVar = append(spliceVar, item) vs. mapVar[key] = item” you’re comparing a slice/array to a map. In the former case, Go is exposing the internals of growing a slice so that the programmer may make the appropriate efficiency trade-offs. Go also has a built-in map type that is used with the familiar “mapVar[key] = item” syntax.

    “How can there be no templates you ask? There just aren’t. I don’t know why.” There are no templates because templates are a tremendously complicated solution to an unspecified problem. Of the many other languages you mention in this post, few of them support templates. I don’t see why Go should be singled out in this regard.

    “Although technically not object-oriented, you can mix in elements of OO as you see fit.” Go is a very object oriented language. Classes != object-orientism.

    “I don’t know what sort of problem this is trying to solve, but I suspect it can just lead to more confusion.” This doesn’t seem to be the case in practice.

    • michael cvet

      Hi Andrew, I’ll reply to your responses in-line below:

      “In your first example you only need one result channel that you receive from n times.”

      You’re certainly right, but I wanted to use this “highly synthetic” example to demonstrate appending to slices and referencing variables in closures. But that is a good point!

      “Of the “ten different ways to declare variables,” you’ve only shown two. In practice there are only two.”

      Well, from the perspective of someone who has just started learning the language (like myself), I demonstrated syntax for several different ways to declare and initialize variables:

      n := len (l) // n’s type is whatever len(l) is
      last := float64(0.0) // explicitly define type through cast of value
      var root float64 = newtons_method (float64(__x)) // using var and type
      x := <- chans[i] // combining initialization declaration with channel read operator

      Its certainly not ten, but that was obviously an exaggeration =)

      "You "can’t put the type before the variable definition" because that would be inconsistent with the rest of the language.""

      That's fair, I'm just not sure how having "x := 0" AND "var x int32 = 0" defined in the language is more much more consistent than having "int32 x = 0" as well. If the syntax is all about consistency, it seems to me that all variable definitions should be something like "var [name] [type] [= initial_value]", which they currently are not. Not to say I don't like the initialization definition.

      "In the example "spliceVar = append(spliceVar, item) vs. mapVar[key] = item" you're comparing a slice/array to a map. In the former case, Go is exposing the internals of growing a slice so that the programmer may make the appropriate efficiency trade-offs. Go also has a built-in map type that is used with the familiar "mapVar[key] = item" syntax."

      Sure. After reading about append() I just expected the map interface to be something like Erlang's dictionary, where you'd do an insert with a statement like dict:store(Key, Value, MyMap). To be fair, to get the size of a map you still need to use len(MyMap). Why not insert(MyMap, key, value)?

      "How can there be no templates you ask? There just aren’t. I don’t know why." There are no templates because templates are a tremendously complicated solution to an unspecified problem. Of the many other languages you mention in this post, few of them support templates. I don't see why Go should be singled out in this regard.

      I should have been a little less specific and stated "generic programming" rather than "templates". But anyways, Java and D support generic programming, and obviously C++ – all presumably the primary audience Go is trying to appeal to. I didn't mean to "single out" Go in this regard. However, generic programming is useful and popular, and in the case of C++ even more so when you consider expression templates. I did just read off of the Go FAQ that implementation of generics are still a topic of discussion within the Go community, so we'll see.

      ""Although technically not object-oriented, you can mix in elements of OO as you see fit." Go is a very object oriented language. Classes != object-orientism."

      I think that my statement is a fair one. I'll quote the FAQ from golang.org: "Is Go an object-oriented language? Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general."

      Go provides facilities for object-orientation if the programmer chooses to use them. They don't have to. I guess I could compare it to OO in C – you can replicate objects if you wish, but the basis of the language is procedural.

      ""I don’t know what sort of problem this is trying to solve, but I suspect it can just lead to more confusion." This doesn't seem to be the case in practice."

      I wonder how many Go programmers have pulled their hair out because their programs failed to compile because they didn't notice some function or variable name didn't start with a capital?

      • Andrew

        “I demonstrated syntax for several different ways to declare and initialize variables:”

        Of those four examples, three of them are the same. In each case where you’ve used colon-equals the type of the new variable is inferred from the type of the expression on the right-hand side. In practice, it’s really not as confusing or complex as you make it out to be. 🙂

        “I’m just not sure how having “x := 0” AND “var x int32 = 0” defined in the language is more much more consistent than having “int32 x = 0″ as well. ”

        It’s consistent because in each valid case the type (explicit or inferred) is on the right.

        “Why not insert(MyMap, key, value)?”

        Because “MyMap[key] = value” is more symmetrical and succinct. The append function is a special case, and it doesn’t modify the slice directly like a map assignment does. Instead, append returns a new slice value.

        I agree that generics in some form would make certain things easier, but we haven’t found a scheme that is congruent with the rest of the language. C++-style templates are out of the question, though.

        What I meant by saying that Go is very object oriented is that goroutines and channels allow you to structure programs in a way that is way closer to Alan Kay’s original concept of object orientism (which centered around message passing). The Java/C++-centric idea that classes are OO is unfortunate. Go doesn’t have a lot of the things that are typically associated with OO, but it is a very object oriented language. (and it is this explanation that the sentence in the FAQ hints at)

        You are right, though, in that Go doesn’t force you to build a class hierarchy to do something simple. One of the nice things about Go is that it tends to get out of your way. 🙂

        “I wonder how many Go programmers have pulled their hair out because their programs failed to compile because they didn’t notice some function or variable name didn’t start with a capital?”

        As an informed guess, I would say “none.” Like all the languages you’ve mentioned, Go’s identifiers are case-sensitive. “Foo” and “foo” are different. If you try to refer to an unexported name, you get this obvious error: “cannot refer to unexported name bar.baz”

  • shazow

    The seemingly random syntax decisions seem to me more like expertly crafted to reduce style ambiguity. For example,
    – Forcing braces on the same line ends the decades-long debate whether they should be on their own line before it starts (see all C-derivative languages that suffer from it).
    – Lack of while loop ends the ambiguity about what kind of loop you should use for what (despite modern compilers converting it into the same instructions in the end).

    I almost wish they got rid of the ambiguous comment formats while they’re at it. I always question my decision when I write comments. Should they be…

    (More consistent but annoying to do multiline)
    // Foo
    // bar

    Or (More Python-style, I usually lean to this)
    /*
    Foo
    bar
    */

    Or (More Java-style, I guess)
    /* Foo
    * bar
    */

    Or the various other slight variations of the above… Ugh.

    Also a couple of random corrections:
    – Don’t use lowercase L as a variable name, looks like uppercase i or 1 or l. Confused me for a few moments (I thought it was a 1).
    – s/No braces/No parenthesis/

    • michael cvet

      Hi shazow,

      I don’t think all the syntax decisions are random, but I do question some.

      For example, if the line-braces issue is such a problem, why not pull a Python and remove them entirely? They did do away with semicolons after all, and if you can’t do anything funny with braces, then you should expect the contents of all in-brace code to look like the same nicely-indented blocks anyways.

      I guess I always just preferred while loops =). I think some expressions are just more natural to express with a while over a for. Besides, its all about what makes it easiest for the programmer and his colleagues to understand, right?

      You’re totally right about comments.

      And good catch about braces/parentheses, I’ll update.

      • Andrew

        “if the line-braces issue is such a problem, why not pull a Python and remove them entirely?”

        Because significant whitespace, as it is implemented in Python, has caused major headaches for some of the Go developers. 🙂 Braces are easy to read. If you format your code with gofmt, all these concerns go away, and your Go code will look like anyone else’s Go code (a huge bonus).

        “I think some expressions are just more natural to express with a while over a for.”

        Do you really care about using the (shorter) word “for” instead of “while”? Really?! 🙂

  • George

    >One of the nice things about Go is that it tends to get out of your way

    I like minimalist spirit of Go. While being performance driven it does much with orthogonal core features, including go-routines, packages and interfaces.

    Thinking more about “getting out of your way” I realized philosophical difference between Go and Perl (soon Perl 6). Perl tries hard to remove barriers from you having your way, but it doesn’t offer safeguards when one forgets to use brakes. The beauty of Go is that it … does.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: