First Impressions and Beginner Tips on Go

Bernard Quek
Rate Engineering
Published in
7 min readOct 30, 2017

Having picked up and worked on Go for the last three months, I’d like to share what I like about Go and what I think is great, based on my first impressions and short experience with it. I’ll also give some low-level tips that are based on some mistakes I’ve made or managed to avoid that might help new Gophers. Hopefully, this can provide insight on what to expect from Go and how to appreciate it better.

This is a very basic article meant for new to intermediate programmers who are curious about Go. For those who have no experience with Go, you can learn the basics here.

Automatic formatting

One of the first things I noticed is that gofmt automatically formats and indents your code for you, even importing and removing the required and unused packages. Not only does this save you time and allows you to focus more on coding, it also acts as a universal style guide that makes it easy to read any other Go code that is similarly formatted.

Automatic semicolon insertion

A godsend feature to some and an annoyance to others, depending on your style. Those in favour of putting their braces on a new-line will not be pleased to hear that it does not work in Go.

func foo()
{
bar()
}
// ... becomes this:func foo(); // Error, missing function body
{
bar();
}
// Instead, do this:func foo() {
bar()
}

Note that the semicolons don’t actually appear in the source code. The lexer will add them for parsing and compiling. Do be careful when defining return values too, for the same reason.

// Wrongfunc Add(a, b int)
(res int) {
return a + b
}
// Correctfunc Add(a, b int) (
res int) {
return a + b
}

Have a look at the semicolon guide to avoid making such mistakes.

Strict typing

Go is a statically typed language. Every variable has a type that is known at runtime. Once a variable is assigned a type, it retains it for its lifetime. As such, you cannot do:

a := 5
a = "Hello, world!" // Error, cannot assign string to int variable
b := add(1, 2)
b := &b // Error, cannot assign *int to int variable
c := &b // Do this instead
// See it in action: https://play.golang.org/p/w0XaW0-_Oe

Furthermore, Go does not have implicit type conversions; you would have to do manual type casting.

var a int
var b int64
a = 3
b = 3
// Wrong
c := a + b // Error, type mismatch for int and int64
s := "You have " + a + " lives left!" // Error, type mismatch for string and int
// Correct
c := int64(a) + b
s := "You have " + strconv.Itoa(a) + " lives left!"
// alternatively
s = fmt.Sprintf("You have %d lives left!", a)
fmt.Println(c)
fmt.Println(s)
// See it in action: https://play.golang.org/p/2U_rVahUig

While this may seem like a hassle (especially if you’re used to weakly typed languages like JavaScript) when you want to quickly print variables for debugging, I find it very helpful as it catches any type-related errors during compilation. Prevention is better than cure, so they say. The more errors the compiler finds, the fewer hairs pulled out while trying to debug runtime errors.

On that note, you’ll also notice that the compiler is very strict, and will refuse to compile if there are unused variables, if you’re re-declaring an existing variables, etc.. It is useful to keep your code clean, though it can be a pain when you’re just tweaking code and trying out small changes.

Error handling

One of my favourite feature of Go is that it does not use try-catch blocks, and instead introduces errors as a value.

// Javatry {
line = br.readLine();
} catch (IOException e) {
// Handle exception
}
// Godata, err := ioutil.ReadFile("file_path")
if err != nil {
// Handle error
}

The code looks nicer without having to wrap parts of your code in try-catch blocks. Using error values also has less overhead and performance issues than their exception counterparts.

Error handling is an important part of any server, and Go encourages you to think about where your code may fail and produce errors, and how to handle them.

func Divide(a, b float) (float, error) {
if b == 0 {
// create error
return 0, err
}
return a / b, err
}
func main() {
x := Divide(5, 3) // Not allowed to ignore error return value
return
}

If you’re insistent, you can get around dealing with errors by assigning the return value to _ , but all this does is make your code unsafe, and expose yourself to runtime bugs.

y, _ := Divide(5, 3)// or evenz, err := Divide(5, 3)
if err != nil {
// do nothing
}

When coding a server, you’ll be validating almost every piece of data that comes your way. Go’s error handling helps you think of all the edge cases that might occur, and have you deal with it, by constantly notifying you when you forget to do proper handling and making it easy to create and return errors.

OOP in Go

Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. — Go Docs

Go has methods that can only be called by a particular type of variable, called receiver methods.

// Java
public class Human {
void talk() {
System.out.Println("Hello, world!");
}
}
Human boy = new Human();
boy.talk();
talk(); //
// Go
func (h *Human) talk() {
fmt.Println("Hello, world!")
}
boy := Human{}
boy.talk()

However, Go does not have inheritance, or at least the way you think it might have. In place of implementation inheritance, we have interface inheritance, where a struct S implements an interface I if S implements all the interface methods found in I. A struct can implement more than one interface.

type Human interface{
Eat()
Sleep()
}
type Combatant interface{
Attack()
Defend()
}
type Soldier struct{}func (s Soldier) Eat() {}
func (s Soldier) Sleep() {}
func (s Soldier) Attack() {}
func (s Soldier) Defend() {}
// Verify that Soldier implements Human and Combatant
// Compiler will notify you of the missing functions otherwise
var _ Human = Soldier{}
var _ Combatant = *Soldier(nil)
// See it in action https://play.golang.org/p/LNQL2cm-kB

In the example above, the Soldier struct implements both Human and Combatant interfaces, and can then implement each of the four methods individually. This way of multiple inheritance is neat as it avoids problems like the Deadly Diamond of Death, while keeping the ability to utilise polymorphism in your code. Any functions that require a variable that implements the Human interface or Combatant interface can accept a Soldier struct as an argument.

Other things

Here are a few more miscellaneous tips when working with Go.

Casting structs

Let’s say we have two Go structs:

type Person struct{
Name string
Age int
}
type Student struct{
ID int
Name string
Age int
}

One might wonder if we are able to cast a Person struct to a Student struct and vice-versa by automatically initialising or removing extra fields. Unfortunately, this is not supported in Go, and casting one struct to another only works if both structs have identical underlying types. To extend a Person struct into a Student struct, you have to take the trouble to convert them and add the extra fields.

func convertPersonToStudent(p *Person) *Student {
s := &Student{
Name: p.Name,
Age: p.Age,
}
s.ID:= GetStudentIDByName(p.Name)

return s
}

It pretty much looks like you’re creating a Student struct from scratch, and just copying the values over from the Person object.

Pointers

Pointers in Go works pretty self-explanatory for the most part.

a := 3
b := &a
*b = 5
fmt.Println(a) // 5

One special note is that struct pointers will de-reference themselves automatically when referencing a field or calling a receiver method. Handy!

type User struct{
Name string
}
func updateUser(u *User) {
u.Name = "Bob" // OK!
}
func (u User) callMe() {
}
func (u *User) callMeAgain() {
}
func main() {
u := &User{}
u.callMe() // OK!
u.callMeAgain() // OK!
}

Check this out for more information.

String concatenation

Go’s strings are immutable. When you concatenate two strings in Go, it actually creates a new string rather than append one to the other. Repeating this operation a large number of times would naturally have bad performance. If you ever need to do this, try joining strings this way:

// Instead of ...s1 := "Hello"
s2 := "World!"
s := s1 + s2
// do ...var buffer bytes.Buffer
s1 := "Hello"
s2 := "World!"
buffer.WriteString(s1)
buffer.WriteString(s2)
s := buffer.String()

This is less costly over a large number of strings.

Conclusion

I like Go. The code is simple, clean, and easy to read, and it has a lot of nice features. The language focuses on getting things done and pleasant to write or write.

There are many other areas of Go that I did not cover, such as concurrency, first class functions and code profiling. If you want to quickly browse the features of Go, you can check out the spec.

The practicality and growing maturity of the language is evident by its popularity — Google, Facebook, Twitter, Uber, Dropbox, and more. If you are looking for something new to add to your set of skills, Go is definitely worth investing time to learn more about.

What are you waiting for? Go out and learn! (Pun intended)

--

--