Page Title Image

Learning Golang

I’ve been primarily a Python developer for the past decade. A job application has asked for the coding interview to be in Golang. This post is to build out my reference in using Go as a Python dev and some sub-projects to get used to Go and Go’s concurrency model.

Table of Contents

Python Differences

Mostly from here

Variables

# Python
some_variable = 2
SOME_CONST = 3.14

# Two variables at once
a, b = 1, "one"
// Go
var someVariable int = 2
const SomeCost float = 3.14

var a, b = 1, "one" // Type is inferred

For Loops

# Python
for i in range(10):
    print(i)
// Golang
// Initial; Condition; After
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

While Loops

# Python
counter = 0
while counter < 5:
    print(counter)
    counter += 1
// Golang

var counter int = 0  // or counter := 0
for counter < 5 {
    fmt.Println(counter)
    counter += 1
}

Arrays/Slices

# Python
my_list = [False, 1, "Two"]
# Golang
// NOTE:  Arrays length is part of its type
var boolArray [3]bool = [3]bool{false, true, true}
var stringArray [3]string = [3]string{"Zero", "one", "two"}

// Slice is dynamic view of an array
var boolSlice []bool = []bool{false} // Var variableName []type.  Supporting array created automatically

The length and capacity can be got through len(s) and cap(s)

Creating a slice with make

Slices can be created with the built in make function. This is how you create dynamically sized arrays

a := make([]int, 5)  // len(a) == 5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

Appending to slices:

var s []int
s = append(s, 0) // Append works on nil slices.  Array will be grown as needed
s = append(s, 2, 3, 4) // Can append multiple values at once.

For loops over containers

# Python
my_list = [False, 1, "two"]
my_dict = {"hello": 0, "world": 1}

for item in my_list:
    print(item)

for key, val in enumerate(my_dict):
    print(f"{key}: {val}")
// Go
var intSlice []int = []int{1,2,3}
for index, value := range intSlice {
    fmt.Println(index, value)
}

var myMap map[string]int = map[string]int{"hello": 1, "world": 2}
for key, value := range myMap {
    fmt.Println(key, value)
}

Maps

my_dict = {"hi": 1, "bye": False}
print(my_dict["hi"])
// Go
var theMap[string]int = map[string]int{"hi": 1, "bye": 2}
fmt.Println(theMap["hi"])

// Making, accessing, deleting, and retrieving from a map
m = make(map[string]int)
m["answer"] = 42
delete(m, "answer")
v, ok := m["answer"]

Functions

# Python
def the_function(first_arg, second_arg) -> Tuple[str, bool]:
    return f"The first arg is {first_arg}, second is {second_arg}", True
// Go
func theFunction(firstArg string, secondArg string) (string, bool) {
    return fmt.Sprintf("First arg is %s, second is %s", firstArg, secondArg), true
}

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // "Naked" return, returns named return values.
}

// NOTE:  theFunction is private, TheFunction is exported

If Statements

if ok := someFunction(x, y); ok {
    // We can have an initializer here, valid for the scope of the if
}

Switch

  • Does not need explicit breaks (always there)
  • Need not be constants. Values need not be integers.
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("macOS.")
case "linux":
    fmt.Println("Linux.")
default:
    // freebsd, openbsd,
    // plan9, windows...
    fmt.Printf("%s.\n", os)
}

Basic Types

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

Pointers

i := 42
p = &i // p points to i

fmt.Println(*p) // Read i through the pointer p
*p = 21 // Set i through pointer.

Structs

type Vertex struct {
    X, Y int
}

func main() {
    fmt.Println(Vertex{1, 2})
    v := Vertex{X: 1} // y is implicitly 0
    v.X = 4  // Fields are accessed using a dot.

    // Structs can be accessed through a struct pointer
    p := &v
    p.X = 1e9  // Language permits just using dot operator without needing to deference

    p = &Vertex{1, 2} // Has a type of *Vertex

}

Function values

// Example shows fn variable requiring a function pointer
func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
    // hypot is a function closure, can access variables in the declared scope
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))

	fmt.Println(compute(hypot))
	fmt.Println(compute(math.Pow))
}

OOP

Methods

type Vertex struct {
	X, Y float64
}

// Operates on a "copy" of a vertex.  Needs to be pointer if doing the object itself
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

Interfaces

defined as a set of method signatures


Errors

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s",
		e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

Generics

Type Parameters

// Index returns the index of x in s, or -1 if not found.
func Index[T comparable](s []T, x T) int {
	for i, v := range s {
		// v and x are type T, which has the comparable
		// constraint, so we can use == here.
		if v == x {
			return i
		}
	}
	return -1
}

Generic Types

// List represents a singly-linked list that holds
// values of any type.
type List[T any] struct {
	next *List[T]
	val  T
}

Go Packages

In Python, packages are in a directory with init.py.

Go requires a go.mod file

go mod init myproject creates the following

module myproject

go 1.25.0

Concurrency

Defering

  • A stack of “deferred statements” is kept until the surrounding function returns
  • Useful for closing files, freeing mutexes, etc, etc.
func main() {
    defer fmt.Println("World")
    fmt.Println("Hello")
}

Channels

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c

	fmt.Println(x, y, x+y)
}

Select

  • Allows us to wait on multiple communications operations
// C and quit are both of type "chan int"
func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
        // We're putting x into c
		case c <- x:
			x, y = y, x+y
        // When quit is available, we exit out of this forever loop
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

Waitgroups

Allows us to wait for multiple Goroutines to finish

import "sync"

var wg sync.WaitGroup

for i := 1; i <= 5; i++ {
    wg.Go(func() {
        worker(i)
    })
}>

Locks and Semaphores

While there are no explicit semaphore, we can use a buffered channel

// Semaphore represents a counting semaphore.
type Semaphore chan struct{}

// Acquire attempts to acquire a slot from the semaphore.
func (s Semaphore) Acquire() {
	s <- struct{}{} // Send an empty struct to acquire a slot
}

// Release releases a slot back to the semaphore.
func (s Semaphore) Release() {
	<-s // Receive from the channel to release a slot
}

func main() {
	maxConcurrency := 3
	sem := make(Semaphore, maxConcurrency) // Create a semaphore with a capacity of 3
    ...
}

Mutex

import "sync"

var mu = sync.Mutex{}
defer mu.Unlock()
mu.Lock()

Atomic counters

import "sync/atomic"

var ops atomic.Uint64
ops.Add(1)

Practice Apps

To practice concurrency and get more comfortable with Go. Here are some example apps.