Now we get to the annoying aspects of Go

|
|

I’ve spent another ten hours or so writing extensive functional tests for my code, and fixing bugs, so I have some further thoughts about Go.

POLA

gotest continues to be easy to use and is versatile, even if you do have to implement a bunch of basic functionality yourself (there are no convenient “assert” functions, there’s no setup or teardown functionality, and nothing like TestNG’s DataProvider mechanism).  Even functional testing of a server process is easier than in other languages, although I think most of that is again due to Go helping me separate my code into modules.  I also found my first non-contrived reason to use goroutines for the first time.  I really like goroutines, but that’s probably because I really like Erlang’s process paradigm.

What really bugs me are the inconsistencies.  Go is self-consistent, which seems to be the Go-team’s answer to any criticism of consistency in the language, which is itself annoying, but it isn’t consistent in the Principle of Least Astonishment way.  In fact, Go is really pretty bad when it comes to POLA.  The thing that kept biting me this weekend – and, admittedly, it was my fault resulting from a lack of experience with Go – was the following case:


  package main
  type Foo map[string]string
  type Bar struct {
    thingOne string
    thingTwo int 
  }
  func main() {
    x := new(Foo)
    y := new(Bar)
    (*y).thingOne = “hello”
    (*y).thingTwo = 1 
    (*x)[“x”] = “goodbye”
    (*x)[“y”] = “world”
  }


Go requires that you know details about the implementation of the types defined above.  x is not really a new object; it’s a pointer to something that doesn’t exist, while y is an actually allocated object.  If you try to use x, you’ll get a runtime fault and your program will crash.  Instead, you have to do this:


    x = make(Foo)


However, if you try to do the same thing with Bar:


    y = make(Bar)


you get a compiler error.  This bothers me, because it’s one of those submarine bugs – if you make a mistake in how you allocate an instance, you won’t find out unless your tests actually hit that code.  I had hoped we’d have gotten away from that by now.  I’d rather Go just made make() work on structures, and then you could eschew new() entirely (unless you really needed it for a specific purpose), and this type of error would become exceedingly rare.  It’s a gotcha; it’s an inconsistency in the syntax.

I’ve mentioned before that I like catching errors as early in the development process as possible, and this is the reason why I prefer strongly typed languages. The sort of problem that I’ve just mentioned weakens Go a bit in this area; for comparison, Haskell eliminates this sort of submarine bug – if you can get your program to compile, it will run, and if it fails, it’ll be entirely due to a logical, algorithmic error, not a type-usage one.  There’s no compiler yet that will check your logic for you, so Haskell is pretty close to perfect, in that respect.

Before anybody comments on my syntax use above, I’ll point out that the dereferencing of the y pointer is unnecessary: Go does that for you.  I did it for illustration purposes.

Pointers

It’s been a long time since I’ve had to use pointers, and during my development and testing I was floundering around trying to determine when and when not to use them.  At one point, I was getting annoyed and was thinking that this was another case of POLA in Go, but it turns out I didn’t have to worry about it.  All you have to do is to ensure that any function that needs to change a structure (or variable) gets passed a pointer, and Go happily figures out the rest.  Another example:

  func (b *Bar) foo() {  b.thing = 1 }
  func (b Bar) write() string { return fmt.Sprint(b) }
  func tralaTrala() {
      var b Bar
      b.foo()
      fmt.Println(b.write())


      a := new(Bar)
      a.foo()
      fmt.Println(a.write())
  }

tralaTrala() does not need to know whether a and b are pointers or not; Go will “do the right thing” here, and the syntax is the same despite the fact that the types of the two variables are different (one’s a pointer, the other isn’t).  The only real issue is that, when writing a function, the programmer does need to know whether foo() and write() modify the state of the Bar they belong to, because s/he needs to know whether to request a pointer or not:

  func doSomething( b Bar ) {
      b.foo()
  }

will probably end up being a runtime logic error, and Go does nothing to help you with these.  This makes me think that the safest way to use Go is to pretend that it’s entirely functional, and write all of your code that way, until you need to optimize it.  This would mean that the signature for foo() would be:

    foo() Bar

and it’d be used like this:

  func doSomething( b Bar ) Bar {
      rv := b.foo()
      return rv
  }

This is another thing that I’m not entirely happy about, but it may be a reasonable trade-off to provide a powerful optimization path.

Copyright © Sean Elliott Russell

comments powered by Disqus