rss resume / curriculum vitae linkedin linkedin gitlab github twitter mastodon instagram
Three things I don't like about Go - Part 1
Mar 21, 2025

Disclaimer: This post includes Amazon affiliate links. Clicking on them earns me a commission and does not affect the final price.

Hello 👋 I’ve been using Go professionally since 2015. I’ve written, seen a lot of Go code, and shipped a lot of services built in Go, but I’ve also made mistakes when using Go.

Since my early beginnings, I’ve been an ambassador of the language, creating content to help other newcomers, and I’ve talked positively about the language.

I understand it is not a perfect programming language, but I’ve never talked about the things I don’t like; that will change now 🙃.

Let’s talk about three things I don’t like about the Go programming language.



Exported variables can be replaced

Because exported variables can be replaced by anyone importing the package you should consider the following:

  • When using packages (either from the standard library or third party), do not use the instance of the exported variable directly, instead instantiate your own, that way you won’t be affected if other packages you’re importing are placing that said exported variables,
  • When implementing packages, if you need to expose a default value then:
    • If the variable is a “basic type” use a constant instead,
    • If you want to return a default implementation, consider creating a method instead of a exported variable, for example a function Default() instead of a variable Default.

Consider a package that has the following exported variable:

3var Default = Calculator{
4	names: map[string]int{
5		"Mario": 10,
6		"Ruby":  15,
7	},
8}

And it’s being used in a Calculator type:

20func (c Calculator) Percentage(name string) int {
21	v, ok := c.names[name]
22	if !ok {
23		return DefaultValue
24	}
25
26	return v
27}

Its default behavior could be changed by replacing the exported Default variable:

15	bonus.Default = bonus.NewCalculator(map[string]int{"Mario": 200, "Ruby": 300})
16	bonus.DefaultValue = 500

To fix this it, my recommendation would be to replace that exported variable with a function, this way it can’t be replaced and it’s always a new instance:

 3func Default() Calculator {
 4	return Calculator{
 5		names: map[string]int{
 6			"Mario": 10,
 7			"Ruby":  15,
 8		},
 9	}
10}

That way it won’t be able to be modified by users importing that package:

10	fmt.Println("Mario", bonus.Default().Percentage("Mario"))
11	fmt.Println("Ruby", bonus.Default().Percentage("Ruby"))
12
13	// Compiler error:
14	//-
15	// bonus.Default = bonus.NewCalculator(map[string]int{"Mario": 200, "Ruby": 300}) // because is function
16	// bonus.DefaultValue = 500  // because is constant

Test Dependencies

Test and tool dependencies are tracked in the same go.mod; thus, downloading them is unnecessary when building a production artifact, there’s a proposal to change the way tests are tracked, and in Go 1.24 tools are already tracked in a different go.mod section.

Let’s assume we are still using the old tools paradigm, in this case sqlcx:

 8import (
 9	_ "github.com/golang-migrate/migrate/v4/cmd/migrate"
10	_ "github.com/sqlc-dev/sqlc/cmd/sqlc"
11)

That will bring those dependencies every time the binary is being built, although in practice the tool is only used for pre-build purposes.

switch statement

switch is a well known statement in Go, that allows grouping similar conditionals via the same expression, however this expression is not required, allowing to write programs that could abuse this statement.

What this means is, take this common switch statement:

15	value := 10
16	switch value {
17	case 2:
18		fmt.Println("It's 2")
19	case 10:
20		fmt.Println("It's 10")
21	}

It uses value as a way to determine what case it should match, but this expression is not enforced, allowing code like the following to still be valid:

44	switch {
45	case errors.Is(err, ErrEmpty):
46		fmt.Println("empty string")
47	case errors.Is(err, ErrTooLong):
48		fmt.Println("too long string")
49	case str == "hello":
50		fmt.Println("said hello")
51	case str == "world":
52		fmt.Println("said world")
53	case err != nil:
54		fmt.Println("other error", err)
55	default:
56		fmt.Println("value was", str)
57	}

The code above checks for two things: errors, same values; and also provides a default behavior; of course it is valid Go code but it makes harder to read when everything is cramped into a switch.

Conclusion

I’ve been using Go for what it looks like forever and although I have a few things I don’t like I still think is a good programming language that can be used for almost all the different use cases we see in real life as Software Engineers, understanding it and knowing the caveats and what we you shouldn’t be doing in certain cases is useful for all Go programmers.

If you’re looking to sink your teeth into more Go-related topics I recommend the following books:


Back to posts