Golang Crash Course from a Pythonista

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.
}

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))
}

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)
}

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))
}

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.

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.

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)

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

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 {
    }
}

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
}

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.")
    }
}

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

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."

Structs

A struct is a collection of fields.

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.

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.

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.

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).

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)

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)

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)
12/2024