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-5 years of expirience
- Learn Go Programming - Golang Tutorial for Beginners by Freecodecamp
- Go 101
- Golang Interfaces Explained
- Learn Go with Tests
- 5+ years of expirience
- 100 Go Mistakes and How to Avoid Them
- Visualize Go slices and arrays
- A Guide to the Go Garbage Collector
- Transitioning from other languages
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.