From .NET to GoLang: Here We Go Again

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():

Make() can be called from main() because it starts with a capital letter.

But if we change the name to start with a lowercase letter, it’s no longer visible:

make(), on the other hand, can only be used by code inside the money package.

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:

You cannot simply assign a value to a struct’s property in a map in Go. Sorry.

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“:

You cannot simply parse a date in an intuitive manner either.

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:

You have to use the weird mnemonic thingy. But you have to chop off the extra text too.

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)
	}
}
Finally. That was a little bit complicated for such a basic operation.

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!

One thought on “From .NET to GoLang: Here We Go Again”

Leave a Reply

Your email address will not be published. Required fields are marked *