Golang quirks for beginner learners
Variable shadowing with “:=” operator
Consider this example:
func f() (int, error) {
return 5, nil
}
func main() {
x := 1
if x, e := f(); e == nil {
println(x) // prints 5
}
println(x) // prints 1
}
This way of writing a compound if
with a variable declaration and a condition in one line is idiomatic in Go. It assigns the variables first, and then checks the condition. Returning a pair with an error from a function is idiomatic too. If the function call and the condition is rather long and complicated, you might be tempted to rewrite it to be on 2 separate lines, and get a side effect:
func main() {
x := 1
x, e := f()
if e == nil {
println(x) // prints 5
}
println(x) // prints 5
}
The problem is twofold:
- The compound
if
actually declares a newx
variable that shadowsx
above. :=
operator is not limited to variable definition. If one of the variables is defined earlier, it just acts as plain “=” assignment.
If you consider a similar in C:
int x = 5;
for (int x = 0; x < 3; x++) {
printf("%d", x); // prints 0, 1, 2
}
printf("%d", x); // prints 5
It produces a compilation warning:
warning C4456: declaration of ‘x’ hides previous local declaration
Remedy
I’m not sure what’s the right solution to this, but several workarounds come to mind. Remember that variables declared after if
belong to their own inner scope (actually any else
block is a part of that scope too). You could try to avoid using :=
inside the if
line, and declare variables in advance with var
. You could try to avoid declaring variables in if
all together. It would be nice to get a warning like the one in C - maybe a GoLang linter can do that?
Struct definition - type
first
It is easy to declare a variable or a constant in Go programming language:
var myVar string = "hello"
const myConst int = 5
The syntax is structured like so:
KEYWORD NAME TYPE = VALUE
“KEYWORD” is like a command that defines what you want to get. “NAME” is an alias to refer later, “TYPE” always goes on the right side.
Now, to define a struct or an interface, you need to type:
type myPoint struct {
x int
y int
}
type myIPoint interface {
x() int
y() int
}
This is a bit odd, because the existing de facto standard syntax in all mainstream popular languages is to write struct myPoint {...}
, i.e. avoid the word type
. Yet this is close to ML family of languages where you write type myPoint = {...}
.
Remedy
To overcome this you need to change the thinking. GoLang makes an emphasis on that you are defining a type “label” first and foremost.
Consider that you also can do this in Go:
func main() {
var mySingletonPoint struct {
x int
y int
}
println(mySingletonPoint.x, mySingletonPoint.y)
}
Here you define a variable mySingletonPoint
that has an anonymous ad-hoc “struct” type. In a similar vain type myPoint struct {...}
can be thought of as if it defines a new type “label” - myPoint
, which always refers to a given anonymous “struct” type.
string is never nil
In general, a “value type” is a type that always has some value (i.e. can’t be “null”), and always passed into functions by value (i.e. copied). Whereas a “reference type” is a type that refers to another value and can refer to nothing (has a special marker value “null”), and is always passed by reference (not copied). In the C language terms int
or struct X
are value types, but pointers types like int *
or struct X *
are reference types. The same applies to GoLang.
What makes Go language different from the crowd is that the built-in string
type is a value type. On the other side slice (array) and map are reference types “as usual”:
func main() {
var s string
var a []int
s = "hello"
a = []int{1, 2, 3}
s = nil
// error: cannot use nil
// as type string in assignment
a = nil
// this is fine
println(s, a)
}
Note that string
is also an array of bytes []byte
under the hood, and you can cast it back and forth (nil
converts to an empty string).
var b []byte = nil
s := string(b)
if s == "" && b == nil {
println("yes") // prints
}
Remedy
Get used to it. If you define a string variable (as var
or a part of a struct
), it gets an empty string ""
value by default. If you want to check if a string value is initialized you need to write if s != ""
.
Interface is a value sometimes
When defining a “class” with methods you have 2 options for declaring “this” parameter: it can be declared as a struct
or as a pointer to the struct
. Having “this” as a pointer (or a reference) is quite typical for many languages and the OOP paradigm where only methods can modify an object, and therefore it has be a modifiable reference, not a copy of the object.
In GoLang you have to think about this:
type ICounter interface {
Add()
Print()
}
type CounterImpl struct {
i int
}
func (this CounterImpl) Add() {
this.i += 1
}
func (this CounterImpl) Print() {
println(this.i)
}
func main() {
var c_impl CounterImpl
var c ICounter
c = c_impl
c.Add()
c.Print() // prints 0
c_impl.Print() // prints 0
}
The example above compiles and runs, but doesn’t do what one would expect from some other programming languages. Assignment to ICounter creates a copy of the original object. Moreover, Add
also creates a copy of this
on the way in, so the increment doesn’t happen.
If you declare “this” as a pointer, you get OO behaviour, but don’t forget to take an address of the struct with “&” operator:
type ICounter interface {
Add()
Print()
}
type CounterImpl struct {
i int
}
func (this *CounterImpl) Add() {
this.i += 1
}
func (this *CounterImpl) Print() {
println(this.i)
}
func main() {
var c_impl CounterImpl
var c ICounter
c = c_impl
// ERROR: cannot use c_impl (type CounterImpl)
// as type ICounter in assignment:
// CounterImpl does not implement ICounter
// (Add method has pointer receiver)
// FIX: c = &c_impl
c.Add()
c.Print() // prints 1
c_impl.Print() // prints 1
}
Remedy
Easy fix: always use a pointer to “this”. This is going to work well most of the time. A non-pointer value can be left for special situations. For example, if you want to ensure that the method is “const” and not modifying anything in the object (allowing immutable objects creation), or if you have special performance considerations.
For loop syntax
Let’s try writing a loop that iterates over a list of values. What can be easier?
list := []string{"a", "b", "c"}
for x in list {
// error: unexpected in, expecting {
println(x)
}
for x := list {
// error: x := list used as value
println(x)
}
The first tries do not work, but someone says that I need to use “range”:
for x := range list {
println(x) // 0, 1, 2
}
This compiles, but doesn’t do what I want. It seems to print indexes that count elements, not values. OK, I give up and google this example:
for i, x := range list {
// error: i declared and not used
println(x)
}
Of course, if i
is not used, you need to use a “placeholder” instead:
for _, x := range list {
println(x) // a, b, c
}
Finally! This is known as “foreach” in some other languages. It is by far the most common type of iteration used in day-to-day programming, thus its syntax could have been simpler.
Remedy
If you are using an IDE, you might be able to bind this construct to a shortcut, or even use a “live template” to customize the variable names. This syntax clashes with other programming languages, and it’s hard to find an easy mnemonic or explanation to remember it. You could still use the classic C syntax if you prefer:
for i := 0; i < len(list); i++ {
x := list[i]
println(x)
}
“go fmt” aligns columns
If you work in a team and respect your colleagues, you want to avoid code merge conflicts with them, you want code review to be “on point”. In this case you are thinking carefully about each change up to the line level. It is a good practice to avoid unrelated line changes, because a lot of standard diff/merge/review tools are line-based.
“go fmt” has a different idea in mind. This example was formatted with this tool and so it has the preferred code style of the Go programming language:
type FormatMe struct {
x int
y int
name string
collectionOfMemories []string
}
func main() {
fm := FormatMe{
x: 1,
y: 2,
name: "test",
collectionOfMemories: []string{"a", "b"},
}
println(fm.name)
}
If you decide to change the longest variable collectionOfMemories
(rename or remove it) - it will produce a “diff” that is 8 lines long, because the right column has to align vertically for all the fields.
Remedy
GoLang programmers are “beaten” by the issue, and there are 2 workarounds to this in practice, but both are somewhat awkward:
- Keep all field names shorter than the longest name. If you don’t exceed the longest name, the right column position doesn’t change. This is awkward, because if you have 3-4 letter names in the struct, you have to invent short 3-4 letter abbreviations. We might get something like “clnt” instead of “client”, “req” instead of “request”, or even “h” instead of “handler”.
- Put a blank line before a new field. This starts a new column for “go fmt” such that the previous field formatting gets disconnected. This is awkward, because it makes you think about field grouping, which often doesn’t make sense, and then you need to put 1 blank line after almost each field.
type FormatMe struct {
x int
y int
name string
collectionOfMemories []string
}
Channels quirks
There are quite a lot of quirks with the Go channels, so they deserve to form a separate post. To be continued…
Cover image by Mathew Bergt under CC BY.
Subscribe to get more articles about programming languages | |
|
|
Follow @battlmonstr | Donate |