Although I’ve been working with Go for less than a year, there are a number of things I’ve learned or noticed since I wrote “From .NET to GoLang: Where Did Everything Go?“, my critique of the Go programming language and ecosystem, three and a half months ago.
As a result, I’d like to use this followup article to discuss some new things that bother me as well as revisit points I already mentioned in the original article.
This is also the 300th article published at Gigi Labs. Hooray!
Productivity
I ended “From .NET to GoLang: Where Did Everything Go?” on a somewhat negative note:
“Coming from C#, learning my way around Go has been a fun diversion, but also something of a disappointment. Given the simplistic, verbose and sometimes confusing language, and the limited standard library, there’s no reason I can think of to choose Go for a new project over something richer and more mature like C#, which nowadays runs on any platform.”
As a result, it is somewhat surprising that I actually feel a lot more productive with Go than I ever did with C#, despite C# being the superior language in my opinion. I think the main reason for this is that .NET software tends to be extremely overcomplicated, owing to a combination of excessive OOP, layers of abstraction, design patterns, and obscenely exploited third party libraries (see “Pitfalls of AutoMapper“) that overzealous software engineers anxious to prove their mettle tend to sprinkle all over the place. On the other hand, Go’s simplicity – the very thing I was complaining about – does not provide many opportunities for this kind of madness.
Stability
I also mentioned that Go’s development seems to be somewhat slow, with particular reference to generics having taken 12 years to appear in the language. But this is also in a way a good thing. Go is a very stable language and upgrading to newer versions is a breeze. I can’t say the same about C# any more, given that every year it’s become a hassle to keep up with the latest unnecessary language ‘features’ as well as breaking changes to ASP .NET Core.
Exception Handling
I talked about Go’s lack of exception handling and the need to have heaps of if
statements to check errors returned by function calls.
Go does in fact have a mechanism similar to exception handling, in the form of panic
and recover
. But since there are no exception objects, this mechanism operates with basic types, just like how the error
interface returns a string and does not otherwise carry structured error information. This is unfortunate because it means it’s impossible for Go to have first chance exceptions, which make the debugger pause as soon as an exception is thrown – an essential troubleshooting tool that I sorely miss from .NET.
Defer
The same blog post I linked about panic
and recover
also talks about defer
, which is the closest thing Go has to .NET’s using
block (to clean up resources that need disposing). In “Scope Bound Resource Management in C#“, I discuss many of the things you can do by using and abusing the cleanup mechanisms in C++ and C#, and you can do most if not all of the same things in Go with defer
.
Exported Names
Go code is organised in packages (like namespaces), but it doesn’t have public or private accessors. Variables and functions can be shared between packages if they start with a capital letter.
Wait, what? Let’s see this in action. I made a really simple program with two packages. I call a function from the money package from main()
:
But if we change the name to start with a lowercase letter, it’s no longer visible:
I’m not at all a fan of such magic conventions, since they are not obvious at all to newcomers (unlike a keyword such as public
would be), in the same way that I’m also not a fan of convention over configuration (used for a long time in ASP .NET Web API, MVC and Core) and content before chrome (used by the disastrous Windows 8 “Metro” theme).
LINQ and Sets
In “From .NET to GoLang: Where Did Everything Go?“, I complained about the standard library’s lack of support for common data structures such as sets, as well as the lack of anything like LINQ, making Go rely a lot on loops and elementary data structures.
However, in both cases, the community has stepped in and filled the gaps. I recently wrote the article “GoLang Set Data Structure“, which describes how to use the third party golang-set library. There is also a go-linq library on GitHub which I haven’t tried yet. Although it’s likely very useful, it’s somewhat unfortunate that Go’s syntax makes its usage a lot less elegant than in C#.
Slices
Go has arrays and slices (the latter being an array-like data structure). You can get a sub-range (also called “slice”) of an array, slice or string just like you can in Python:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
jonathan := "Jonathan"
fmt.Println(numbers[1:3]) // [2 3]
fmt.Println(jonathan[:3]) // Jon
}
Go, however, doesn’t allow negative indices:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
fmt.Println(numbers[1:-3]) // compiler error
}
Whereas with Python you can use negative indices to denote an index starting from the end (handy when you want to e.g. trim off the last character of a string):
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> numbers[1:-3]
[2, 3]
Achieving the same thing with Go is a little more verbose, which is not very surprising at this point:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
fmt.Println(numbers[1 : len(numbers)-3]) // [2 3]
}
Else Braces
I did mention in the original article that Go is a little fussy about braces used with an if
statement, both in terms of their necessity and placement. Where it gets really annoying, though, is when it fails to build because you put the else
on the next line, like this:
package main
import "fmt"
func main() {
if ("sky" == "blue") {
fmt.Println("Not in Ireland")
}
else { // compiler error
fmt.Println("Probably Ireland")
}
}
The pedantic bastard requires you to put the else on the same line as the preceding brace, like this:
package main
import "fmt"
func main() {
if "sky" == "blue" {
fmt.Println("Not in Ireland")
} else { // compiler is happy
fmt.Println("Probably Ireland")
}
}
Whatever makes you happy, dear Go compiler.
Syntax (Revisited)
I wrote at length in the original article about how I find Go’s variable declaration syntax to be very confusing at best. There’s an article on the Go blog explaining the reasoning behind Go’s Declaration Syntax, which is informative but frankly not very convincing.
I also wrote about my aversion to the :=
syntax. My feelings are not just a matter of style but also come from having been screwed a couple of times by this fancy operator. Let’s see a simple if somewhat absurd example:
package main
import "fmt"
func uptown() (string, error) {
return "Uptown Funk!", nil
}
func main() {
lyrics := "Eye of the Tiger"
if "sky" != "fuchsia" {
lyrics, err := uptown()
if err != nil {
// nobody cares, Bruno
}
fmt.Println(lyrics)
}
fmt.Println(lyrics)
}
What do you think the output will be? OK, I’ll tell you. It’s:
Uptown Funk!
Eye of the Tiger
I used the := operator because the err
variable is new and needs declaring. But because of that same operator, the lyrics
variable inside the if
statement is an entirely new variable that shadows the one outside. So once we’re back outside the if
statement, thinking we updated the value of the lyrics
variable, we find that it still has its original value.
I’m not even sure what is the best way to deal with this situation, though an easy way to update the outer lyrics
variable is to use an intermediate variable to capture the output of uptown()
and then assign that to lyrics
:
package main
import "fmt"
func uptown() (string, error) {
return "Uptown Funk!", nil
}
func main() {
lyrics := "Eye of the Tiger"
if "sky" != "fuchsia" {
innerLyrics, err := uptown()
if err != nil {
// nobody cares, Bruno
}
fmt.Println(innerLyrics)
lyrics = innerLyrics
}
fmt.Println(lyrics)
}
Addressable Values
Let’s say I have a map
whose value is a struct
. For instance:
package main
type Person struct {
Hobby string
}
func main() {
people := map[string]Person{"James": {Hobby: "Drinking"}}
people[i].Hobby = "Farting"
}
I create a map
with a single value which maps the key “James” to a struct
, and then I try to update a property on that struct
. Seems simple enough? And yet, it doesn’t compile:
What?! Why? It seems like this boils down to this concept of Addressable values in Go, WHICH FRANKLY I DON’T CARE ABOUT BECAUSE NO OTHER LANGUAGE EVER HAD A PROBLEM UPDATING A PROPERTY LIKE THIS!
To update that value, as far as I can tell, you instead have to take the detour of replacing the entire object in the map:
func main() {
people := map[string]Person{"James": {Hobby: "Drinking"}}
updatedJames := Person{Hobby: "Drinking"}
people["James"] = updatedJames
}
Dates & Times
In “GoLang Timestamps: A Cross-Platform Nightmare“, I wrote about a very specific problem I encountered with Go time. But why don’t we talk about something more common? Parsing timestamps.
Let’s try to parse a date/time in a way that any reasonable language would allow, i.e. using placeholders like “yyyy-MM-dd
“:
No, Go doesn’t use such placeholders. Why would it? Instead, it uses this crazy mnemonic device to represent the various parts of the date:
Mon Jan 2 15:04:05 MST 2006
If you look closely, you’ll notice that these are actually the numbers 1 through 7:
- Jan (1) is the month
- 2 is the day
- 15 (3) is the hour
- 4 is the minutes
- 5 is the seconds
- 2006 (6) is the year
- MST (-7) is the time zone relative to UTC
So, the equivalent of parsing as “yyyy-MM-dd
” would be something like:
Oops, that didn’t work either. I have to chop off the extra bits for it to work:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(time.Now()) // 2022-11-16 22:23:39.354436468 +0100 CET m=+0.000014317
parsedTime, err := time.Parse("2006-01-02", "2022-11-16")
if err != nil {
fmt.Println("Failed to parse time: ", err)
} else {
fmt.Println("Parsed time: ", parsedTime)
}
}
Fortunately, there are a few predefined layouts you can use, such as time.RFC3339
. But the ubiquitous RFC 8601 is nowhere to be found, so you’ll likely have to beg Stack Overflow for that.
Conclusion
It’s certainly been educational, entertaining and occasionally frustrating to learn about Go’s quirks. And considering I haven’t been working with it that long, I’m sure there will be lots more to come!
Nice that you began seeing around and outside of the .NET realm.
Imho, someone, like myself, who comes from .NET/C# background, does not seem to grasp the idea of open source in a wider spectrum. Let me make a strong statement: we simply do not understand the open source in it’s truest form.
Why? Because, we only see and use things which come from Microsoft. Thats it!
While in languages like Go, there are tons of libraries out there. We only need to look around and understand how these work and hook them in the solution where necessary.
Here comes another criticism of .NET/C# from my side: in Microsoft/.Net Foundation’s introductions to the .Net libraries and API, we only get to hear how cool these are and how cool that Microsoft implemented/integrated those features/libraries/APIs in C#, rather than getting to know and understand what problems do they solve.
Lately, I have seen C# getting syntactically closer to Python. I am afraid that a few years, C# developers would end up writing Python actually.
Maybe, I am wrong in this criticism of mine and I do hope so!