Impressions of Go: From the guy who learned it in 2 days

tung's picture

Go is that new-fangled language out of Google, and though I won't describe it (been done much better by somebody else), I will give a few opinions from the point of view of somebody who's written some code in it.

C-like syntax

It looks like C. Braces, parens and semi-colons all make yet another cameo in a programming language. The upside is that I can carry over a lot of my C muscle memory, which means less example consultation during the initial stages and more doing.

The downside is that, well, it's a C-ish syntax. Again. Is this really the best we can do? Maybe it is. Who knows?

Semi-colons as separators

Here, have some Go:

func something() {
    fmt.Println(1);
    fmt.Println(2)    // <- no semi-colon!
}

Yeah, that's right, semi-colons are statement separators. For some unknown reason, this drives me nuts. From C, I'm used to looking at a statement with a semi-colon at the end and verifying it as "good". What happens when this is violated?

I keep leaving off semi-colons where I should have them, in the course of normal programming.

func something() {
    fmt.Println(1)    // <- oops, needs semi-colon
    fmt.Println(2)
}

The problem isn't that I always used to add semi-colons, it's that the new nature drives me to leave them out at the end, 'in the spirit of the language'. It's the second-statement-if-body problem turned token, which is funny because that problem was eliminated by ifs requiring braces.

Thankfully, you can still use them as terminators, and Go doesn't complain.

func something() {
    fmt.Println(1);
    fmt.Println(2);    // <- this is fine too
}

All loops are for loops

I don't like the C for loop: it doesn't express the semantics of, really, any kind of iteration with the fidelity that specialised looping constructs do. Naturally, my first reaction to "for loops only" was something resembling disgust. But it turns out that it doesn't come out too badly because it takes multiple forms.

// traditional for
for i := 0; i < 5; i ++ {
    // stuff
}
// infinite loop
for {
    // stuff
}
// for-each loop over a map of strings to strings
for key, value := range map[string] string { "a": "apple", "z": "zebra" } {
    // stuff
} 

The for loops of Go do closer reflect what you're doing, so I'm good with them.

Coroutines? More like Goroutines.

Back in my programming languages course I ran into Occam, based on Hoare's CSP. Despite my keenness for the course, I didn't pick it up in the one tutorial that I faced it.

CSP resurfaces here in Go in the form of channels: you can send stuff in one end and get it out in a completely different place. Different thread, even. Which raises the question: how do I threads?

Enter goroutines, Go's cute answer to the coroutine. I won't explain them here, but the idea is that you can run any function with the "go" keyword, and that launches it in its own very inexpensive thread.

How do goroutines communicate? Channels!

/** Get the first 10 even numbers. */
 
func something() chan int {
    out := make(chan int);
    go func () {
        for i := 0; ; i += 2 {
            out <- i;
        }
    }();
    return out;
}
 
func something_else() {
    even_numbers := something();
    for n := 0; n < 10; n++ {
        print(<-even_numbers);
    }
}

The big surprise is how easy it is to write code this way. Loops that would have to be manually maintained and inverted can suddenly be written the way they're thought about. Funky!

"Make programming fun again."

I was working on a little HTTP server for delivering files, and wanted to pull up a REPL to try out a language feature. That is, until I remembered that Go is a compiled language. Have I spent too long with Python and Ruby? Or does Go really feel as productive as one of these languages?

The sizes

Here's that 30-something line HTTP file server that always delivers files in the text/plain MIME format.

package main
 
import (`http`; `io`)
 
func main() {
    err := http.ListenAndServe(":12345", FileServeMux(OpenFile));
    if err != nil {
        panic("ListenAndServe: ", err.String());
    }
}
 
func OpenFile(c *http.Conn, req *http.Request) {
    url := req.URL.String();
    if url[0] == '/' {
        url = url[1:len(url)];
    }
    if buf, error := io.ReadFile(url); error == nil {
        c.SetHeader("Content-Type", "text/plain; charset=utf-8");
        c.Write(buf);
    } else {
        // Let's pretend this is always 404.
        c.SetHeader("Content-Type", "text/plain; charset=utf-8");
        c.WriteHeader(http.StatusNotFound);
        io.WriteString(c, "404 not found\n");
    }
}
 
// El-cheapo file serving muxer. So cheap it doesn't deserve a struct.
type FileServeMux func (*http.Conn, *http.Request);
 
func (this FileServeMux) ServeHTTP(c *http.Conn, req *http.Request) {
    this(c, req);
}

I take a lot of shortcuts here, for example, all errors are treated as 404s.

EDIT: I acknowledge that this code is terrible. The default HTTP muxer was suitable after all, I just didn't realise it only matches on prefixes and leaves the URL itself intact.

But look how it comes out:

[tung@eee ~/Code/Go]$ ls -lh servefiles*
-rwxr-xr-x 1 tung users 990K 2009-11-15 18:14 servefiles
-rw-r--r-- 1 tung users  20K 2009-11-15 18:13 servefiles.8
-rw-r--r-- 1 tung users  912 2009-11-15 16:13 servefiles.go

Geez, 990 KB!? That's pretty friggin' huge. Check out hello world for comparison:

[tung@eee ~/Code/Go]$ ls -lh hello*
-rwxr-xr-x 1 tung users 569K 2009-11-15 18:14 hello
-rw-r--r-- 1 tung users 5.1K 2009-11-15 18:13 hello.8
-rw-r--r-- 1 tung users   75 2009-11-15 11:18 hello.go

Hello world weighs in at an, um, impressive 569 KB. Wow.

To be fair, Go is still in its early stages. I'm guessing that Go is statically linking its libraries into the binaries it produces, so if it were able to do dynamic linking a la traditional shared object files, I'm guessing these binaries wouldn't nearly be as huge.

EDIT: Looking at the object file sizes versus the linked binaries, it seems like that's almost precisely what's going on. Some shared object strategy with dynamic linking would help a lot here.

Still, recent articles on the intertubes have linked program performance and binary size, so if Go is serious about getting in plinking range of C's speed, binary size will have to be addressed at one point or another.

Conclusions

Go, despite the recent public fanfare, has been cooking quietly for about 2 years prior. If history is anything to go by, it takes about a decade for any new programming language to gain steam, but the standard library is shaping up pretty well, and with the big names behind it, the language has a promising future.

It ain't perfect. Types aren't first class, so there are no generics. That's still in the works, but use of the empty interface feels like C's void pointers or Java's collections using Object handles. Also notably absent are macros, though I definitely wouldn't want C's macros to be blindly reproduced. Thankfully, code replication is minimised with anonymous functions (aw yeah) and lexical closures (double aw yeah).

EDIT: Anonymous functions and lexical closures don't eliminate code replication, but they do make it easier to write other things compactly and cleanly.

Long story short, I could see myself using this seriously.