A while ago, I mentioned I'd ported a hobby project from CoffeeScript to Go. Since this is now the DailyWhatTheGo forums, I've been meaning to post my own experience. Here you go, in no particular order of importance: (I've used C-like pseudocode to illustrate the concepts except where the syntax is the point of the WTF)
Exceptions aren't thrown in the traditional sense. They're returned. (Technically you _can_ throw them, but this is a bad practice and, for example, you won't write a library that threw exceptions because your caller won't expect it to.) So a function that wants to call 3 functions in serial would look like
result1, err = func1() if (err != null) return err
result2, err = func2(result1)
if (err != null)
return errresult3, err = func3(result2)
if (err != null)
return err
and so on. There are no C macros like Windows has for HRESULT's, so no way to avoid writing this.
Go is supposed to force you to check for exceptions (in the same spirit as Java's checked exceptions). Functions can have multiple return values and it is convention to return a tuple of (result, error) where one and only one is non-null. There is also a syntax for destructured assignment.
result, err = someFunctionThatCouldFail() if (err == null) return err; // Bubble up exception // else result is valid. Use it...
Writing
result = someFunctionThatCouldFail()
is a compiler error. So in this way, the programmer is kinda forced to not ignore exceptions. Good.
However, if someFunctionThatCouldFail doesn't actually have a return value but can still throw an exception, then you'd call it as
err = someFunctionThatCouldFail()
Go allows you to ignore the return value in this case, which means the lazy programmer could just as well write
someFunctionThatCouldFail() someOtherFunction()
and this would call someOtherFunction() regardless if someFunctionThatCouldFail() succeeded or not.
And here I thought the days of On Error Resume Next were behind us. This language has it on by default!Continuing with exceptions, C# exceptions have a string message and a stack trace. Java exceptions have a string message and a stack trace. Go exceptions have a string message. No stack trace. Just the equivalent of a ToString(). So even if you do bubble your "someFunctionThatCouldFail() failed!" exception all the way to your topmost catch-all error handler/logger, it's going to have no idea where the exception actually came from. You could of course put some form of source line information manually in the message, but that still doesn't give you a stack trace of where it was called from. Good luck debugging!
Go comes with a few libraries to help you get a basic HTTP server started. You set up a server and register handlers on certain URLs; pretty standard stuff. One of the available handlers is a static files handler. But it's totally untweakable:
- It has a hard-coded mapping of file extension to Content-Type.
- You can't tell it to not serve certain file types.
- You can't control the output. It renders a flat series of <a> tags without any surrounding <html> or <body> or any other HTML.
When sending a response from an HTTP server, in most other libraries, you either have the freedom to set the status code and the headers in any order, or you have to set the status code first and the headers second. In Go's http module, you have to set the headers first and the code second.
Another touted benefit of Go: You can compile binaries for all platforms from any platform! Except on Windows, it wants gcc to compile for Linux. Oops!
Go has pointers. Go has interfaces. A type T can implement interfaces. A pointer to T, T*, ... can implement interfaces. Any interfaces it wants. Totally unrelated to what are implemented by T. In fact, this is how the Exception interface works, it's implemented by pointers, not by actual types. That is, if you have a custom exception type MyException, then it's not MyException that implements the Exception class's ToString(), it's MyException*
Related to the above, Go sometimes allows you to write foo.bar where you actually should write (*foo).bar Only sometimes, though. So if you want to be sure, you'd probably write it as the latter all the time just to be consistent.
Want to initialize a variable? foo := bar
Want to assign a variable a new value? foo = bar
The compiler knows perfectly well when you assign a value to an uninitialized variable, so this annoying difference is totally cosmetic and pointless.
(Yes, I know it's significant if you want to make a new variable to shadow one in an outer scope. Personally I'd rather that shadowing wasn't allowed at all. I've never come across any instance in all my software dev life where shadowing was _necessary_.)
On the subject of classes, there aren't any. There are structs. Remember how in C, you don't define member functions on a struct? Instead you define them outside and usually put the struct type as the first parameter? That's how it's done in Go too.
Similarly, Go doesn't have constuctors. The convention to have a constructor for a type Foo is to make a static NewFoo method. Of course, this doesn't prevent the caller from ignoring it and calling new Foo() themselves, so when looking at API docs, always remember to check if a NewFoo method exists or not first! And if you're writing a class, better throw any guarantee that your object will be in a consistent state out of the window!
Function overloading isn't allowed either. I sure hope you only need one set of parameters for your constructor!
No generics either.
There's a printf (format string, varargs). There's a println (single string). There's no println that takes a format string and varags, so you have to write "\n" in all of your println's, since 99% of the usages of println are for writing lines.
Just like the C++ STL, there's no rational thought put behind when something should be a member function and when something should be a static function on the type. So methods like string.Contains are actually static methods on the strings type.
Speaking of strings, how do you append a character to a string? First you have to call a method to convert the character to a string. Then you call a method to append the new string to the original string.
Go is supposed to enforce strict whitespacing via compiler errors, and some of them indeed are. Good. (This isn't about the gofmt tool.) But a lot of them aren't. Bad. It's very inconsistent on what it does think is an error and what isn't.
Windows is pretty much a third-class citizen right now. For example, a function that takes in a path string and returns the path after resolving all symlinks returns the input path unmodified on Windows. Looking at the source commits, there is a commit that claims to have fixed it but actually didn't. Of course, one of the touted benefits of Go is that you can compile binaries for all platforms from any platform! I'll just compile a binary for Linux and test there. Oh wait. On Windows, it wants a gcc to be able to compile for Linux. Oops!
Now Go does have a lot of nice features. It compiles very fast, and the channels are very nice. Unfortunately the language is full of warts that totally make it unworkable, and the attitude of the zealots on the mailing list (basically the only place apart from the API docs where you can get any answers) makes it difficult for me to bother. I may be wrong on some of these because I only spent a week or so on this before giving up, so I'll be happy to be corrected.