The Artima Developer Community
Sponsored Link

Java Buzz Forum
On Go

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
Bill de hÓra

Posts: 1137
Nickname: dehora
Registered: May, 2003

Bill de hÓra is a technical architect with Propylon
On Go Posted: Jun 1, 2013 7:48 PM
Reply to this message Reply

This post originated from an RSS feed registered with Java Buzz by Bill de hÓra.
Original Post: On Go
Feed Title: Bill de hÓra
Feed URL: http://www.dehora.net/journal/atom.xml
Feed Description: FD85 1117 1888 1681 7689 B5DF E696 885C 20D8 21F8
Latest Java Buzz Posts
Latest Java Buzz Posts by Bill de hÓra
Latest Posts From Bill de hÓra

Advertisement

This is one of a series of posts on languages, you can read more about that here.

Herein a grab bag of observations on the Go programming language, positive and negative. First, a large caveat - I have no production experience with Go. That said I'm impressed. It's in extremely good shape for a version 1. To my mind it bucks some orthodoxy on what a good effective language is supposed to look like; for that alone, it's interesting. However beyond that, It makes sensible engineering decisions and is squarely in the category of languages I consider viable for the kind of server systems I work on.  

You can find more detail and proper introductory material at http://golang.org/doc. And this is my first observation - the language has great documentation.

What's to like?  

I’m gonna go get hammered with Papyrus

Syntax matters. In all languages I've used and with almost all teams, there's been a need to agree syntax and formatting norms. And so, somebody, invariably, prudently, fruitfully, gets tasked with writing up the, to be specific, company style guide. Go eschews this by supplying gofmt, which defines mechanically how Go code should be formatted. I've found little to no value in formatting variability with other languages - even Python which constrains layout more than most doesn't goes far enough to lock down formatting. It's tiring moving from codebase to codebase and adjusting to each project's idiom - and open source now means much of the code you're working with isn't written to your guidelines. Another benefit of gofmt is that it makes syntax transformations easier - for example the gofix tool is predicated on gofmt. This isn't as powerful as a type driven refactoring but is nonetheless useful. So while I gather it's somewhat controversial, I like the decision to put constraints on formatting. 

yak

As is the way with recent idiom in languages, semi-colons are gone, no suprise there. Interestingly, there's no while or do, just for, something I like so far. If statements don't have parens, which took me a while to get used to, but have come to make visual sense. If statements on the other hand, don't have optional bracing, which I really like. A piece of me dies every time someone removes the carefully crafted if braces I put around one-liners in Java. Go doesn't have assertions - having seen too much sloppy code around assertions, specifically handling the failure itself I think this is a good decision. The import syntax using strings (like Ruby's require statement), something I'm not crazy about; but what's interesting is that these are essentially paths, not logical packages.

Declaration order reverses the usual C/C++/Java way of saying things.  This is similar to Scala and is something I like because it's easier when speaking the code out - saying 'a of type Int' is less clumsy than 'the Int named a' - the latter sounds like a recording artist wriggling out of a contract. Go has simple type derivation when using the ':=' assignment operator, albeit somewhat less powerful than again say, Scala inference.

Syntactically the language reminds me of JavaScript and Groovy, but feels somewhat different to either.

You had just one job

There are no exceptions in Go and no control structures at all for error handling. I find this a good decision. I've believed for a long time that checked exceptions are flawed idea. Thankfully this isn't controversial anymore and all's right with the world. Runtime exceptions are not much better.Maybe finally is ok, I could fall in with that. 

So you handle errors yourself. Go has an inbuilt interface called error - 

type error interface {
    Error() string
}

and allows a function to return it along with the intended return type, which has a surface similarity with multiple return values in Groovy/Python. Now, I can see this kind of in place error handling driving people insane -

if f, err := os.Open(filename); err != nil {
 return err
}

but I prefer it to try/catch/finally, believing that a) failures and errors are commonplace, b) what to do about them is often contextual such that distributing occurence and action isn't the right default. Pending some improved alternate approach or a real re-think on exceptions, it's better not to have the feature. 

Because there are no exceptions there's no finally construct - consequently you're subject to bugs around resource handling and cleanup. Instead there is the 'defer' keyword, that ensures an expression is called after the function exits, providing an option to release resources with some clear evaluation rules. 

Go has both functions and methods and they can be assigned to variables to provide closures. Functions are just like top level def methods in Python or Scala and can used directly without an enclosing class structure via an import. Methods are defined in terms of a method receiver. A receiver is instance of a named type or a struct (among others). For example the sum function below can be used against integers whenever it is imported -

package main
import "fmt"
func sum(i int, j int) (int, error) {
  return i + j, nil
}
 
func main() {
    s, err := sum(5,10)
    if err != nil {
     fmt.Println("fail: " + err.Error())
    } 
    fmt.Println(s)
}

Alternatively, to make a method called 'plus' the receiving type, is stated directly after 'func' keyword -  

package main
import "fmt"
type EmojiInt int
func (i EmojiInt) plus(j EmojiInt) (EmojiInt, error) {
  return i + j, nil
}
 
func main() {
    var k EmojiInt = 5
    s, err := k.plus(10)
    if err != nil {
     fmt.Println("fail: " + err.Error())
    } 
    fmt.Println(s)
}

This composition drive approach leads us into one the most interesting areas of the language, that's worth its own paragraph.

Go does not have inheritence or classes. 

If it helps at all, here's a surprised cat picture -

While OO isn't clearly defined, languages like Java and C# are to a large extent predicated on classes and inheritence. Go simply disposes of them. I'd need more time working with the language to be sure, but right now, this looks like a great decision. Controversial, but great. You can still define an interface -

type ActorWithoutACause interface {
    receive(msg Message) error
}

and via a structural typing construct, anything that implements the signatures is considered to implement the interface. The primary value of this isn't so much pleasing the ducktyping crowd (Python/Ruby developers should be reasonably ok with this) and support of composition, but avoiding the premature object hierarchies typical in C++ and Java. In my experience changing an object hierarchy is a heavyweight maintenance effort, and it requires effort to avoid creating one early. These days I'm even reluctant to define Abstract/Base (implementation inheritance) types - I'll use a DI library to pass in a something that provides the behaviour and I'd go as far as saying I'd even prefer duplicated code in early phase development to establishing a hierarchy (like I said it requires effort). Go lets me dodge this problem to a large extent.

This ain't chemistry, this is art.

 

Dependency seems to be a strong focus of the language. Depedendecies are compiled in, no dynamic linking. Go does not allow cyclic depedencies. Consequently build times in Go are fast. You can't compile against an unused dependencie - eg this won't compile -

package main
import "fmt"
import "time"
 
func main() {
    fmt.Println("imma let u finish.")
}
prog.go:3: imported and not used: "time 

Which seems pedantic but scales up well.  Go builds are lightning fast, fast enough to be used for short scripts. You can use the dependency model to fetch remote repositories, eg via git. I have more to say on that when it comes to externalities.

Package visibility is performed via capitalization of the function name. Thus Foo is public, foo is private.  I'll take it over private/public/protected boilerplate. I  would have gone for Python's _foo idiom myself, but that's ok, it's obvious what's what when reading code. 

Go doesn't have an implicit 'self/this' concept, which is great for avoiding not just scoping headaches a la Python, but also silly interview questions. When names are imported, they are prefixed - 

package main
import "fmt"
import "time"
 
func main() {
    time.Sleep(30)
    fmt.Println("imma let u finish.")
}

Imported names are qualified, all unbound names are in the package scope. Note how I still have to qualify Sleep and Println with their time and fmt packages. I love this, it's one of my favorite things about the language. If you dislike static imports in Java as much as I do and the consequent clicking through in the IDE to see where the hell a name came from, you may also like what Go does here.

Go has pointers to enable by reference and by value approaches, but thankfully no pointer math, so JVM types like myself don't need to freak out on seeing the '*' and '&' symbols (and in case you're wondering arrays are bounds checked). There aren't any numeric coercions, so you can't do dodgy math over different scalar types.  This isn't really a feature because languages that allow easy coercions like this are arguably broken. That said, given Go's design roots in C and C++, I'm happy to see that particular brokeness wasn't brought forward. It still won't stop anyone using int32 to describe money however.  

You have just saved yourself from a fate worse than the frying pan

The concurrency model in go is based on CSP rather than shared memory. There are synchronization control structures available in the sync and atomic packages (eg CAS, Mutex) but they don't seem to be the focus of the language. In terms of mechanism, Go uses channels and goroutines, rather than Erlang-style actors.  Channels are are sort of typed queue, buffered or unbuffered, assigned to a variable. You can create a channel and use goroutines to produce/consume over it. To get a sense of, here's a noddy example - 

package main
import "fmt"
import "time"
 
func emit(c chan int) {
    for i := 0; i<100; i++ {
        c <- i
        fmt.Println("emit: ", i)
    }
}
 
func rcv(c chan int) {
   for {
        s := <-c 
        fmt.Println("rcvd: ", s)
    }
}
 
func main() {
    c := make(chan int, 100)
    go emit(c)
    go rcv(c)
    time.Sleep(30)
    fmt.Println("imma out")
}

Channels are pretty cool. They give a nice hygenic foundation for building things like server dispatch code and event loops. I could imagine build a UI with them.  With actor models the process receiver gets a name (in Erlang for example, via a pid/bif), whereas with channels the channel is instead the thing named. 

As you can see above it's easy enough to use goroutines - call a function with 'go' and you're done. Once they're created goroutines communicate via channels, which is how the CSP style is achieved. That said you can use shared state such as mutexes but the flavour of the language is to work via channels. Goroutines are 'lightweight' in the Erlang sense of lightweight rather than Java's native threads. Being 'kinda green' they are multiplexed over OS threads. Go doesn't parallelize over multiple cores by default as far as I can tell; in that sense it's closer to Node.js than Erlang. It is possible to widen Go and allot more native threads to leverage the cores via the runtime.gomaxprocs global, which says 'this call will go away when the scheduler improves'; it will interesting to see what happens in future releases. Otherwise, the approach to scaling out seems to be to use rpc to dispatch across multiple go processes for now.  As best as I can tell a blocking goroutine won't block others as the other goroutines can be shunted onto another native thread, but my knowledge of the implementation is insubstantial so I might have the details wrong. 

Externalities

Onto some things that bother me about Go. 

Channels are not going to be as powerful as Actors used in conjunction with pattern matching and/or a stronger type system. Mixing channel access with case statements seems possible if you want to emulate an actor style receive model, but it'll be lacking in comparison Erlang and Scala/Akka that underpin their various actor models. That said, channels seem more than competitive in terms of the all import concurrency/sanity tradeoff when compared to thread based synchronization. I can't imagine wanting to drop into threads after using channels.

The type system in Go is antiquated. If you're bought into modern, statically typed languages such as Haskell, Scala OCaml and Rust and value what they give you in terms of program correctness, expresiveness, and boilerplate elimination, Go is going to seem like a step backwards. It is probably not for you. I'm sympathetic to this viewpoint, especially where efforts are made to match static typing with coherent runtime behaviour and diagnostics, not just correctness qualities. On the other hand if you live in the world of oncall, distributed partial failures, traces, gc pauses, machine detail, and large codebases that come with their very own Game of Code social dynamics, modern static typing and its correctness assurances don't help much. Perhaps the worst aspect with Go is that you are still subject to null pointers via nil; trapping errors helps but not as much as a system that did its best to design null out, such as Rust

Non-declaration of interface implementation feels in a kind of chewy yet insubstantial way, like a feature that isn't going to scale up. I imagine this will be worked around with IDE tooling providing implements up/down arrows or hierachy browsers. Easier composability via structural typing is possibly a counter-argument to this if the types in the system interact with less complexity than sub-type heavy codebases, something that's achievable to a point with Python/Scala but impractical in Java. So I'm ready to be wrong about this one. 

Go concurrency isn't safe, for example, you can deadlock. Go is garbage collected, using a mark/sweep collector. While it's probably impossible for most of us to program concurrent software and hand manage memory, my experience with Java is a lot of time dealing with GC, especially trying to manage latency at higher percentiles and overflowing the young generation. Go structs might allow better memory allocations, but I don't have the flight time to say GC hell will or won't exist in Go. It would be very interesting to see how Go holds up under workloads witnessed by datastores such as Hadoop/HBase, Cassandra, Riak and Redis, or modern middlewares like Zookeeper, Storm, Kafka and Rabbitmq.

The 'go get' importing mechanism is broken, at least until you can specify a revision number. I'd hazard a guess and say this comes from Google's large open codebase, but I've no idea what the thinking is. Having worked in a codebase like that I can see how it makes sense along with a stable master policy. But I can also see stability will suffer from an effect similar to SLA inversion, in that the probability of instability is the product of your externally sourced dependencies being unstable. It's important to think hard about your dependencies, but in practice if you have make an emergency patch and you can't because you can't build because of upstream changes you are SOL. A blameless post-mortem that identified inability to build leading to a sustained outage is going to result in a look of disapproval, at best. I don't see how to protect from this except by copying all dependencies into the local tree and sticking with path based imports, or using a library based workaround. The former complicates bug propagation in the other direction resulting in rot. Put another way using sneaky pincer reasoning - if Go fundamentally believed this was sane, the language and the standard libraries wouldn't be versioned. Thankfully it's a mechanism rather than a core language design element and should be something that can get fixed in the future. 

Conclusions

Go seems to hit a sweetspot between C/C++, Python JavaScript, and Java, possibly reflecting its Google heritage, where those languages are I gather, sanctioned. It does seem to be trying to be a better language, rather it seems to be trying to be a more effective one, especially for in-production use. 

Should you learn it and use it? Yes, with two caveats. How much you like static type systems, and how much you value surrounding tooling.

If you really value modern, powerful type systems as seen in Haskell, Scala and Rust, I worry you'll find Go pointless. It offers practically nothing in that area and is arguably backwards looking. Yes there is structural typing, and (thankfully) closures, but no sum types, no generics/existentials, no monads, no higher kinds, etc - I don't think anyone's going to be doing lenses and HLists in Go while staying sane. 

An issue is whether significant investment into the Go runtime and diagnostic tooling will happen outside Google. Tools like MAT, Yourkit, Valgrind, gcviz etc are indescribably useful when it comes to running server workloads. The ecosystem on the JVM for example, in the form of the runtimes, libraries, diagnostic tools, and frameworks, is like gravity - if anything was going to kill Java, it was the rise of Rails/Python/PHP in the last decade - that didn't happen. I know plenty of shops are staying on the JVM, or have even moved back to Java, mostly because of its engineering ecosystem. This regardless of the fact the language has ossified. JVMs have been worked on for nearly two decades, by comparison Go's garbage collector is immature, and so on.  

A final thought. A lot of code written today is in languages that can be considered to have a similar surface to Go - C, C++, C#, Python JavaScript, and Java. If you buy into the hypothesis that programming language adoption at the industry level is slow and highly incremental, then Go's design center is easy to justify and it success possible, given either a killer application (a la Ruby on Rails for Ruby and browsers for JavaScript) or a set of strong corporate backers who bet on the language (a la Java and C#). Aside from generics and possibly annotations, there's a reasonable argument to be made that Go is sufficiently more advanced than Java and JavaScript without being conceptually alien, and good enough compared to C#, Python and C++. Plenty of shops don't make decisions based on market adoption, but for larger engineering groups it's inevitably a concern.  

Read: On Go

Topic: Got my feet wet with Clojure Previous Topic   Next Topic Topic: Continuous Delivery – Part 4 – A/B Testing

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use