Post

Transition to Go as a Java, C#, Typescript or other engineer

Initial struggles with Go

Junior, Mid, Senior, or even more expirienced engineer - it does not matter. If you have some expirience with some OOP language, embarking on journey with Go feels like navigating through uncharted waters. The shift from the Object-Oriented Programming (OOP) paradigm, where everything fits neatly into classes and patterns, has been both liberating and challenging. Go introduces fundamental differences—no more inheritance, a breath of fresh air, and the elevation of functions to first-class citizens.

But worry not, the journey would be a reward one. Go is awesome!

Learning resources

I think a big challenge, that most starting with Go have, is finding relative resources. I will try to split this by level of expirience “needed”, however with a enough effort one can finish any read regarless of their expirience.

  • All
  1. Roadmap: Go Developer
  2. Go Class by Matt KØDVB
  3. How to “Go project folder structure”
  • 1-5 years of expirience
  1. Learn Go Programming - Golang Tutorial for Beginners by Freecodecamp
  2. Go 101
  3. Golang Interfaces Explained
  4. Learn Go with Tests
  • 5+ years of expirience
  1. 100 Go Mistakes and How to Avoid Them
  2. Visualize Go slices and arrays
  3. A Guide to the Go Garbage Collector
  • Transitioning from other languages
  1. Go for Java Developers
  2. Introduction to Go for senior (Python) developers

Some Go peculiarities

Forget about immutability.

Go does not have built-in support for immutability. Variables are mutable by default.

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
    var x int
    x = 5
    fmt.Println(x) // Output: 5

    x = 10
    fmt.Println(x) // Output: 10
}

Forget about getters/setters overall.

Go encourages direct access to struct fields.

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

type Person struct {
    FirstName string
    LastName  string
}

func main() {
    p := Person{FirstName: "John", LastName: "Doe"}
    fmt.Println(p.FirstName) // Direct access, no need for getters
}

Do not use panic for errors - return errors instead.

Use error wrapping and errors.Is/As for error type checking.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func modifyValue(x int) {
    x = 10
}

func modifyPointer(x *int) {
    *x = 10
}

func main() {
    value := 5
    modifyValue(value)
    fmt.Println(value) // Output: 5

    modifyPointer(&value)
    fmt.Println(value) // Output: 10
}

Nil slices/maps are ok, even best practice.

You don’t need to return empty ones for example.

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
    var slice []int
    var m map[string]int

    fmt.Println(slice == nil) // Output: true
    fmt.Println(m == nil)     // Output: true
}

Zero values are automatically assigned to variables when they are declared.

Embrace zero values and make use of them. Forget about “optional” and nil-avoidance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

type Point struct {
    X int
    Y int
}

func main() {
    var p Point
    fmt.Println(p.X, p.Y) // Output: 0 0
}

Early returns everywhere.

Use early returns to reduce indentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func validateInput(x int) error {
    if x < 0 {
        return fmt.Errorf("input cannot be negative")
    }
    return nil
}

func main() {
    err := validateInput(-5)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Println("Validation successful")
}

Concurrency - learn channels and select statement well.

When using a sync.Mutex or concurrent data structures, ask yourself if you can solve the problem using channels instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func validateInput(x int) error {
    if x < 0 {
        return fmt.Errorf("input cannot be negative")
    }
    return nil
}

func main() {
    err := validateInput(-5)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    // Rest of the code if validation is successful
    fmt.Println("Validation successful")
}

Use context package for timeouts/cancellations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "context"
    "fmt"
    "time"
)

func performTask(ctx context.Context) error {
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        // Do some work
        time.Sleep(2 * time.Second)
        return nil
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    if err := performTask(ctx); err != nil {
        fmt.Println("Error:", err)
    }
}

Accept interfaces, return structs (Not always possible, but learn it)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}

func calculateArea(s Shape) float64 {
    return s.Area()
}

func main() {
    circle := Circle{Radius: 5}
    result := calculateArea(circle)
    fmt.Println("Area:", result)
}

Mind the package names when naming structs/interfaces.

For the package backup, the type Store could be good name for a type, not BackupStore. From other packages, it would read as backup.Store.

Use defer

but don’t use defer INSIDE a for loop.

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    fmt.Println("Start")
    defer fmt.Println("Deferred statement")
    fmt.Println("End")
}

Summary

The article discusses transitioning to Go engineering from languages like C# or Java, emphasizing the shift from object-oriented programming (OOP) to a module-based approach.

What is OOP? It is the idea about grouping your code into classes.

Instead of organizing code into classes, Go focuses on module organization, where functions can be part of a struct or standalone within the appropriate module.

Properly defined modules, with limited size and interdependencies, allow developers to focus on individual modules without being concerned about external factors, similar to working on a class in OOP.

This post is licensed under CC BY 4.0 by the author.