Python to Go Crash Course for Python Developers

File Structure

Executable programs must have a main function, and the package name must be defined as follows:

package main

func main() {
  // Do something here.
}

In Python, there's no strict entry point requirement, but the common pattern is:

if __name__ == '__main__':
    # Do something here.

The difference is that Go enforces this at the language level โ€” your program won't compile without package main and func main(). Python's if __name__ is just a convention.

Imports

Packages can be imported using their relative paths like this:

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    fmt.Println("My favorite number is", rand.Intn(10))
}

Python's import syntax is similar but has more flexibility:

import math                    # Go: import "math"
from math import sqrt          # Go: no equivalent, you always use math.Sqrt()
import math as m               # Go: no built-in aliasing (well, there is but rarely used)

One key difference: in Go you must use every imported package or the code won't compile. Python doesn't care about unused imports.

Visibility

Items within packages that start with capital letters are exported and can be accessed by other packages. This will raise an error because pi is not exported:

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(math.pi)
}

In Python, visibility is just a convention: prefix with _ to mark something as "private." But nothing actually prevents you from accessing _private_var from outside. Go enforces this at the compiler level โ€” lowercase names are truly inaccessible from other packages.

Functions

In Golang, functions are defined using the func keyword instead of def. Parameters and return types must be explicitly defined, like this:

package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(42, 13))
}

Python equavelent with type hints:

def add(x: int, y: int) -> int:
    return x + y

print(add(42, 13))

The big difference: Go's type declarations are enforced by the compiler. Python's type hints are just annotations โ€” they don't prevent you from passing a string where an int is expected. You need tools like mypy to actually check them.

You can also simplify parameter type declarations by only specifying the type for the last parameter. The preceding parameters will be assumed to be of the same type. In this example, both x and y are integers:

func add(x, y int) int {
    return x + y
}

Named Return Values

In Golang, return values can be named. In this example, the split function returns two named values, x and y. These names can help document the purpose of the return values:

package main

import "fmt"

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

func main() {
    fmt.Println(split(17))
}

If you do not name your return values, the function will use a "naked" return. Avoid naked returns as much as possible, especially in larger functions, as they make code harder to read and understand.

Python doesn't have named return values, but you can get close with type hints and tuple returns:

def split(total: int) -> tuple[int, int]:
    x = total * 4 // 9
    y = total - x
    return x, y

print(split(17))

Go's named returns also serve as documentation โ€” you can see what each return value means from the function signature. In Python you'd need a docstring or a NamedTuple for that.

Variables

the var statement is used for defining variables. Like arguments in functions you can define multiple variable types by giving type to last one. In this example, foo, bar and baz is booleans.

var foo, bar, baz bool

If variables are not initialized, their defaults are:

  • 0 for numeric types,
  • false for the boolean type, and
  • "" (the empty string) for strings.

Variable Scopes

Variables scoped by packages and functions. and they can be initialized like this:

var foo, bar, baz bool = true, true, false

When you're creating variables with initializers, you don't need to explain their types. This will give the same result:

var foo, bar, baz = true, true, false

If you're using variables with initializers, defining a variable can be shortened like this:

foo, bar, baz := true, true, false

When you use :=, you don't need to use var statement.

In Python, you just assign directly โ€” no var keyword, no type declaration needed:

foo, bar, baz = True, True, False

Python also has type hints if you want to be explicit, but they're optional:

foo: bool = True
bar: bool = True
baz: bool = False

Another difference: Python variables don't have zero values. If you try to use a variable before assigning it, you get a NameError. Go gives you 0, false, or "" depending on the type.

Types

Here is the list of types in Golang:

  • 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

Note: The int, uint, and uintptr types are usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems. When you need an integer value you should use int unless you have a specific reason to use a sized or unsigned integer type.

Type Castings

The expression NewType(OldTypeVariable) converts the value OldTypeVariable to the NewType.

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

Python's type casting looks almost identical:

i = 42
f = float(i)
u = int(f)

Same syntax pattern โ€” Type(value). The difference is Go does this at compile time and Python at runtime. Also Python is more forgiving: int("42") works in Python but Go can't convert a string to int with a simple cast (you need strconv.Atoi).

Constants

Constants are same with variables but they are not mutable. They can be string, number or boolean values. const keyword used to define constant instead of var.

const pi float = 3.14

Python doesn't have real constants. The convention is to use UPPER_CASE names, but nothing stops you from changing the value:

PI = 3.14  # Convention only, you can still do PI = 42

Python 3.8+ has typing.Final but it's only a hint for type checkers, it doesn't actually prevent reassignment at runtime. Go's const is enforced by the compiler.

Loops

Other languages has while and for loops but Golang has only for loop. But it can be used to construct while loop and infinitive loops too.

Most basic usage is like the javascript, in same line you initialize the variable, set the condition to continue loop and an executor to change your variable. Only difference is you don't put them inside a pharantesis.

package main

import "fmt"

func main() {
    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    fmt.Println(sum)
}

By the way you dont need to initialize your variable in for loop, you can use pre initialized variable.

package main

import "fmt"

func main() {
    sum := 1
    for ; sum < 1000; {
        sum += sum
    }
    fmt.Println(sum)
}

And you dont need to type your executor to the for loop. This usage is basically while loop in other languages:

package main

import "fmt"

func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}

And... you can create infinite loop by using for loop without any parameters like this:

package main

func main() {
    for {
    }
}

Python has separate keywords for these patterns:

# C-style for loop โ†’ Python uses range()
for i in range(10):
    sum += i

# While loop
while sum < 1000:
    sum += sum

# Infinite loop
while True:
    pass

I actually like Go's approach here โ€” one keyword, three patterns. Python's for and while are more readable at first glance but Go's unified for is elegant once you get used to it.

Control Flows

If Statements

If statements are like for loops but you only create a condition

package main

import (
    "fmt"
    "math"
)

func sqrt(x float64) string {
    if x < 0 {
        return sqrt(-x) + "i"
    }
    return fmt.Sprint(math.Sqrt(x))
}

func main() {
    fmt.Println(sqrt(2), sqrt(-4))
}

Like walrus operator in Python, you can define variable and check that in same line in golang:

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    }
    return lim
}

Python equavelent using the walrus operator (3.8+):

import math

def pow(x, n, lim):
    if (v := math.pow(x, n)) < lim:
        return v
    return lim

The syntax is different (:= in Go's if statement vs := as Python's walrus operator) but the idea is the same: assign and check in one line. Note that in Go, the variable v is scoped to the if block only. In Python, v leaks into the surrounding scope.

Switch statements

Switch statements are used to create sequence of if else conditions like other languages. Switch statements are evaluated from top to bottom.

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.\n", os)
    }
}

Also you can use predefined variables to create switch blocks:

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}

Python 3.10+ has match-case which is similar but more powerful (it supports pattern matching). For older Python versions, you'd use if-elif-else:

import platform, datetime

# match-case (Python 3.10+)
match platform.system():
    case "Darwin":
        print("OS X.")
    case "Linux":
        print("Linux.")
    case other:
        print(f"{other}.")

# if-elif-else (classic Python, same as the second Go example)
hour = datetime.datetime.now().hour
if hour < 12:
    print("Good morning!")
elif hour < 17:
    print("Good afternoon.")
else:
    print("Good evening.")

One thing to note: Go's switch doesn't fall through by default (you need fallthrough keyword). Most other languages do fall through by default, so this is actually a nice Go design decision.

Defers

I didn't see this concept before. Defers are functions to call when a caller function is finished. When a function is finished, it's deferred calls are executed last in first out order:

package main

import "fmt"

func main() {
    fmt.Println("counting")

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
}

This code will print 0 to 9 after main function is finished.

The most common use of defer is closing files or connections. Its basicly Go's answer to Python's with statement:

file, err := os.Open("data.txt")
if err != nil {
    return err
}
defer file.Close()
// work with file, it will be closed when function ends

Python equavelent:

with open("data.txt") as file:
    # work with file, it will be closed when block ends

Also used for unlocking mutexes, closing database rows, closing HTTP response bodies etc. Everywhere you'd use with or try/finally in Python, you use defer in Go.

Pointers

Go supports pointers, which store the memory address of a value.

  • The & operator is used to obtain the pointer to a variable.

    i := 42
    p = &i
    
  • The * operator is used to access or update the value the pointer refers to.

    fmt.Println(*p) // reads the value of i through the pointer p
    *p = 21         // updates the value of i through the pointer p
    

This process is called "dereferencing" or "indirecting."

Python doesn't have pointers. Everything in Python is a reference to an object โ€” you never deal with memory addresses directly. The closest thing to understanding pointer behavior is knowing the difference between mutable and immutable types:

# Mutable (like passing a pointer in Go)
a = [1, 2, 3]
b = a
b.append(4)
print(a)  # [1, 2, 3, 4] โ€” both point to the same list

# Immutable (like passing a value in Go)
x = 42
y = x
y = 100
print(x)  # 42 โ€” x is unchanged

In Go, you explicitly choose whether to pass by value or by pointer. In Python, this behavior depends on whether the object is mutable or immutable. You don't get a choice.

Structs

A struct is a collection of fields. They are basicly classes without methods in Python.

package main

import "fmt"

type Vector struct {
    X int
    Y int
}

func main() {
    v := Vector{1, 2}
    v.X = 4
    fmt.Println(v.X)
}

They are initialized with {} and their values are accessed by dot. If you're familiar with Python's dataclass, structs are pretty much the same thing:

from dataclasses import dataclass

@dataclass
class Vector:
    x: int
    y: int

v = Vector(1, 2)
v.x = 4
print(v.x)

Actually dataclass is a better comparison than regular classes because structs also give you equality comparison and string representation for free, just like dataclass does with __eq__ and __repr__.

Pointers to Structs

When you're accessing struct instances via pointers, you can access it's fields without using *

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    p := &v
        // (*p).X = 1e9 < -- You can do it like that but there's a shortcut:
    p.X = 1e9 // This is the shortcut.
    fmt.Println(v)
}

Arrays and Slices

In Golang there are two kind of data types which is used contain multiple values of a type: arrays and slices. Arrays are primitive, they contain static number of elements but they are slightly faster. Slices are most commonly used, they can be resized, changed etc.

Arrays:

  • Perform slightly better when their size is fixed.
  • Minimal runtime overhead.
  • Commonly used for low-level and fixed-size data structures.

Slices:

  • More flexible and practical.
  • Adds a minimal abstraction layer over arrays, which introduces slight runtime overhead.
  • Preferred in most cases, as they better align with Go's standard data structures.

If the size is fixed and doesn't change often, consider using an array. However, if a dynamic size or flexibility is required, slices are the better choice.

Arrays

In golang, arrays can not be resized. O_o They are defined as [number of elements]Type. For example var a [10]int creates an array called a with 10 integers.

Python doesn't have fixed-size arrays in the language. The closest thing is a tuple (immutable, fixed-size) or the array module (typed but resizable). In practice, you'd just use a list and... not resize it:

a = [0] * 10                 # "array" of 10 zeros
# or
from array import array
a = array('i', [0] * 10)     # typed array of 10 integers
# or
a = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)  # tuple, truly immutable

Slices

In golang slices are used create sliced views for arrays. They are defined as []Type = Array[low: high]. For example: var s []int = a[1:4] creates a s variable which is slice from the a array by getting first and fourth elements.

Python also has slicing syntax (a[1:4]), but there's a critical difference: Python slices create copies, Go slices are references. This trips up a lot of Python developers:

names = ["John", "Paul", "George", "Ringo"]
a = names[0:2]   # Creates a NEW list ["John", "Paul"]
a[0] = "XXX"
print(names)      # ["John", "Paul", "George", "Ringo"] โ€” unchanged!

Note that slices are not copies, they are references. The referenced array is changed, slice is changed too. Here is an example that creates names array and creates two slices from it. When you check the output you can see that when referenced array (names) are changed, a and b slices are changed too:

package main

import "fmt"

func main() {
    names := [4]string{
        "John",
        "Paul",
        "George",
        "Ringo",
    }
    fmt.Println(names)

    a := names[0:2]
    b := names[1:3]
    fmt.Println(a, b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(names)
}

Slice Literals

Slice literals are shorcut for defining an array and getting a slice from it IMO.

This is an array literal:

[3]bool{true, true, false}

And this creates the same array as above, then builds a slice that references it:

[]bool{true, true, false}

Here is a bigger example. In this case we're creating 3 slices q, r and s without defining their arrays. Also you can see that we can use structs to define array types.

package main

import "fmt"

func main() {
    q := []int{2, 3, 5, 7, 11, 13}
    fmt.Println(q)

    r := []bool{true, false, true, true, false, true}
    fmt.Println(r)

    s := []struct {
        i int
        b bool
    }{
        {2, true},
        {3, false},
        {5, true},
        {7, true},
        {11, false},
        {13, true},
    }
    fmt.Println(s)
}

Slices are dynamically sized, so this usage is actually to create array like structures which are dynamically sized. You can add or remove items from slices instead of arrays.

When slice low and high points are not described, lowest and highest available points will be used. For example if a is an array with 10 elements, a[:10] means a[0:10], or a[3:] means a[:10].

Slice Length and Capacity

A slice has two key properties: length and capacity.

  • Length: The number of elements the slice currently holds.
  • Capacity: The total number of elements available in the underlying array, starting from the first element of the slice.

You can retrieve the length and capacity of a slice s using the expressions len(s) and cap(s). Python has len() but no concept of capacity โ€” Python's list handles memory allocation internally and doesn't expose it to you.

Slices can be extended by re-slicing them, as long as they don't exceed their capacity.
Try modifying one of the slice operations in the example program to extend the slice beyond its capacity and observe the result.

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // Slice the slice to give it zero length.
    s = s[:0]
    printSlice(s)

    // Extend its length.
    s = s[:4]
    printSlice(s)

    // Drop its first two values.
    s = s[2:]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

Making Slices

There's a command called make, which is used to create an empty slice with given size.

a := make([]int, 5)

Personal rant: Why is make a thing? You dont need it for variables, you dont need it for arrays, but suddenly for slices you need a special builtin function? Something like var s []int(0, 10000) would be just as explicit and way more consistent with the rest of the language. I think make exists because Go's type system cant handle constructor parameters, so they had to bolt on a separate function. Not great design IMO.

You can also define it's capacity (maximum size). In this example we have a slice with 5 zeros and it\'s maximum capacity is 10.

a := make([]int, 5, 10)

Now heres the thing about slices that confused me at first: a slice is basicly a struct with 3 fields: a pointer to an underlying array, a length, and a capacity. When you append to a slice and it exceeds capacity, Go creates a brand new array (usually 2x the size), copies everything over, and points the slice to the new array. Sounds expensive right?

But actually Python's list does the exact same thing under the hood. CPython's list_resize function does the same grow-and-copy dance. The difference is Python hides this from you, Go doesnt.

So if you care about performance and you know how many elements you'll need, preallocate the capacity:

// You know you'll have 10000 elements? Tell Go upfront.
s := make([]int, 0, 10000)
// Now all 10000 appends will be zero-copy, single allocation.

In Python's builtin list you dont have this option, it grows however it wants. You can use numpy to preallocate memory but thats a third party library. In Go this is just how slices work out of the box.

Slices inside slices

Slices can contain other slices. Here's an example of tic tac toe game board. It\'s simply a 2D array.

package main

import (
    "fmt"
    "strings"
)

func main() {
    // Create a tic-tac-toe board.
    board := [][]string{  // [][] --> Slice contains slices.
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }

    // The players take turns.
    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }
}

This is equavelent for this in Python:

board = [
  ['_', '_', '_'],
  ['_', '_', '_'],
  ['_', '_', '_']
]

board[0][0] = "X"
board[2][2] = "O"
board[1][2] = "X"
board[1][0] = "O"
board[0][2] = "X"

for row in board:
  print(' '.join(row))

Actually this is the long way to define board in Python you can just do this:

board = [['_'] * 3] * 3

Appending

In Golang appending to a slice is done by append function. I'm not sure why is this not a method of a slice but probably lack of methods causing this. Append function takes slice as first parameter rest of parameters are things to add to that slice.

var s []int
printSlice(s)
s = append(s, 1)
printSlice(s)

This is equavelent to this code block in Python:

s = []
print(s)
s.append(1)
print(s)

Range

Range is enumuerate in Python. Used to run over loops for every element and their indexes.

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

This is equavelent to this code block in Python

pow = [1, 2, 4, 8, 16, 32, 64, 128]
for i, v in enumerate(pow):
  print(2 ** i, '=', v)

Using index and value together is not madatory, you can by pass one of them. For example by passing the index:

var lst = []str{'A', 'B', 'C'}
for _, value := range lst {  // Prints values
    fmt.Printf(value)
}
for i, _ := range lst {  // Prints indexes
    fmt.Printf(i)
}

This is equavelent to this code block in Python:

lst = ['A', 'B', 'C']
for _, value in enumerate(lst):
  print(value)
for i, _ in enumerate(lst):
  print(value)

Maps

Maps are Go's equavelent of Python dictionaries. They map keys to values. You create them with make or with a map literal. The syntax is a bit ugly compared to Python's {} but it works:

package main

import "fmt"

func main() {
    m := make(map[string]int)
    m["alice"] = 25
    m["bob"] = 30
    fmt.Println(m)
    fmt.Println(m["alice"])

    // Delete a key
    delete(m, "bob")
    fmt.Println(m)

    // Check if key exists
    val, ok := m["bob"]
    fmt.Println(val, ok) // 0 false
}

This is equavelent to this code block in Python:

m = {}
m["alice"] = 25
m["bob"] = 30
print(m)
print(m["alice"])

del m["bob"]
print(m)

val = m.get("bob")
print(val, val is not None)  # None False

Map literals look like struct literals but keys are required. Honestly its not that different from Python's dict syntax:

m := map[string]int{
    "alice": 25,
    "bob":   30,
}

Which is the same as Python's:

m = {"alice": 25, "bob": 30}

Closures

Go functions can be closures. A closure is a function that references variables from outside its body. This is basicly same as Python closures:

package main

import "fmt"

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    c := counter()
    fmt.Println(c()) // 1
    fmt.Println(c()) // 2
    fmt.Println(c()) // 3
}

Python equavelent:

def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

c = counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

Notice that Python needs the nonlocal keyword to modify the outer variable, Go doesnt need anything special.

Methods

Go doesnt have classes. Yeah, you read that right. No classes. Instead you define methods on types using a receiver argument. Its basically attaching a function to a struct and calling it a day:

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

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

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func main() {
    c := Circle{Radius: 5}
    fmt.Println(c.Area())
    fmt.Println(c.Perimeter())
}

Python equavelent:

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

c = Circle(5)
print(c.area())
print(c.perimeter())

If you want a method to modify the struct, use a pointer receiver:

func (c *Circle) Scale(factor float64) {
    c.Radius *= factor
}

Without the pointer (*Circle), the method would work on a copy and changes wont persist. In Python you never have to think about this because self is always a reference. One more thing to worry about in Go. :/

Interfaces

Interfaces in Go are implemented implicitly. If a type has all the methods that an interface requires, it automaticly implements that interface. No implements keyword needed:

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct {
    Radius float64
}

type Rectangle struct {
    Width, Height float64
}

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

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func printArea(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
}

func main() {
    printArea(Circle{Radius: 5})
    printArea(Rectangle{Width: 3, Height: 4})
}

Python equavelent using abstract base classes:

from abc import ABC, abstractmethod
import math

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

def print_area(s):
    print(f"Area: {s.area():.2f}")

print_area(Circle(5))
print_area(Rectangle(3, 4))

The big difference: in Python you explicitly inherit from Shape. In Go, Circle and Rectangle implement Shape automaticly just by having an Area() method. This is called "structural typing" or basicly "duck typing at compile time." Pretty cool IMO.

Error Handling

Go doesnt have try/except. Instead functions return errors as a second return value. This is probably the biggest culture shock for Python developers:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("nonexistent.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer file.Close()
    fmt.Println("File opened:", file.Name())
}

Python equavelent:

try:
    file = open("nonexistent.txt")
    print("File opened:", file.name)
    file.close()
except FileNotFoundError as err:
    print("Error:", err)

In Go, the pattern is always the same: call a function, check if err != nil, handle it. You'll write this pattern hundreds of times. Coming from Python this feels like a massive step backwards. I mean, try/except is one of the best things about Python and they just... didnt add it? Instead you get the same 3 lines of boilerplate after every single function call. O_o

Anyway, you can also create custom errors:

import "errors"

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 0)
if err != nil {
    fmt.Println(err) // division by zero
}

Python equavelent:

def divide(a, b):
    if b == 0:
        raise ValueError("division by zero")
    return a / b

try:
    result = divide(10, 0)
except ValueError as err:
    print(err)

Goroutines and Channels

This is where Go really shines compared to Python. Goroutines are lightweight threads managed by the Go runtime. You start one by just putting go before a function call:

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Println("Hello from", name)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go sayHello("goroutine")
    sayHello("main")
}

Python equavelent using threading:

import threading
import time

def say_hello(name):
    for _ in range(3):
        print(f"Hello from {name}")
        time.sleep(0.1)

t = threading.Thread(target=say_hello, args=("thread",))
t.start()
say_hello("main")

OK I have to admit, this is where Go actually impresses me. Goroutines are much cheaper than Python threads. You can spawn thousands of them without any issues, and they actually run in paralel (no GIL!). This is probably the main reason people switch to Go from Python.

Channels

Channels are how goroutines communicate. They are typed pipes you can send and receive values through:

package main

import "fmt"

func sum(numbers []int, ch chan int) {
    total := 0
    for _, n := range numbers {
        total += n
    }
    ch <- total // Send total to channel
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    ch := make(chan int)
    go sum(numbers[:5], ch)
    go sum(numbers[5:], ch)

    a, b := <-ch, <-ch // Receive from channel
    fmt.Println(a, b, a+b)
}

Python equavelent using queue.Queue:

import threading
import queue

def sum_numbers(numbers, q):
    q.put(sum(numbers))

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
q = queue.Queue()

t1 = threading.Thread(target=sum_numbers, args=(numbers[:5], q))
t2 = threading.Thread(target=sum_numbers, args=(numbers[5:], q))
t1.start()
t2.start()

a = q.get()
b = q.get()
print(a, b, a + b)

Select

Select lets a goroutine wait on multiple channel operations. It's like a switch statement but for channels:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "one"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println("Received from ch1:", msg)
        case msg := <-ch2:
            fmt.Println("Received from ch2:", msg)
        }
    }
}

Python doesnt have a direct equavelent for this. The closest thing would be using asyncio.wait with FIRST_COMPLETED:

import asyncio

async def task1():
    await asyncio.sleep(0.1)
    return "one"

async def task2():
    await asyncio.sleep(0.2)
    return "two"

async def main():
    tasks = [asyncio.create_task(task1()), asyncio.create_task(task2())]
    for coro in asyncio.as_completed(tasks):
        result = await coro
        print(f"Received: {result}")

asyncio.run(main())

Packages and Modules

Go uses modules for dependency management. Every Go project starts with go mod init:

$ mkdir myproject && cd myproject
$ go mod init github.com/username/myproject

This creates a go.mod file (similar to Python's requirements.txt or pyproject.toml). When you import external packages, go mod tidy downloads them:

$ go mod tidy

Your project structure looks like this:

myproject/
โ”œโ”€โ”€ go.mod
โ”œโ”€โ”€ go.sum        # lock file (like pip freeze)
โ”œโ”€โ”€ main.go
โ””โ”€โ”€ utils/
    โ””โ”€โ”€ helpers.go

Importing your own packages:

// main.go
package main

import (
    "fmt"
    "github.com/username/myproject/utils"
)

func main() {
    fmt.Println(utils.Add(1, 2))
}
// utils/helpers.go
package utils

func Add(a, b int) int {  // Uppercase = exported
    return a + b
}

Python equavelent structure:

# myproject/
# โ”œโ”€โ”€ pyproject.toml
# โ”œโ”€โ”€ main.py
# โ””โ”€โ”€ utils/
#     โ”œโ”€โ”€ __init__.py
#     โ””โ”€โ”€ helpers.py

# main.py
from utils.helpers import add
print(add(1, 2))

# utils/helpers.py
def add(a, b):
    return a + b

Key difference: Go uses capitalization for visibility (uppercase = public, lowercase = private). Python uses the _ prefix convention. I find the capitalization thing weird, it took me a while to stop naming everything lowercase out of habit.

String Formatting

Go uses fmt.Sprintf for string formatting, which works like C's sprintf. After using Python's f-strings this feels like going back to 2005 but whatever. As a Python developer I find %v (default format) the most useful verb:

name := "Mirat"
age := 33
s := fmt.Sprintf("My name is %s and I'm %d years old", name, age)
fmt.Println(s)

Python equavelent:

name = "Mirat"
age = 33
s = f"My name is {name} and I'm {age} years old"
print(s)

Common format verbs in Go:

Verb Description Python equivalent
%v Default format {} in f-strings
%s String %s or {:s}
%d Integer %d or {:d}
%f Float %f or {:f}
%t Boolean -
%T Type of value type()
%+v Struct with field names repr()