A space for me to learn go.
Find a file
2026-06-06 18:48:12 +02:00
assets docs: add example of an interface (image from yt video) 2026-06-04 15:04:51 +02:00
fibonacci feat: create fibonacci algorithm 2026-05-24 09:39:37 +02:00
http feat(http): add additional endpoint 2026-05-24 15:10:25 +02:00
tour feat(tour): save draft of "exercise-loops-and-functions" 2026-05-24 20:27:48 +02:00
.gitignore Initial commit 2026-05-19 20:53:50 +02:00
gin.md docs(gin): how to initialize gin project + example of simple router 2026-06-04 15:06:05 +02:00
go.mod feat: some hello-world file 2026-05-19 21:42:43 +02:00
hello.go feat: some hello-world file 2026-05-19 21:42:43 +02:00
README.md docs: explain testing basics 2026-06-06 18:48:12 +02:00
tour.md docs(tour): notes about using "A Tour of Go" 2026-05-23 11:22:50 +02:00

go

A space for me to learn go.

General

  • Go code is written in .go files

  • Go does automatic formatting of your code on saving

  • A package is a collection of files & functions into something whole.

  • A go project can be run with go run <DIRECTORY> eg. go run . for the current directory.

  • To run a project you need to have a main() function. By default the main() function will be run.

  • Its the base function for your project

  • Check your installed go version with go version

    go version go1.26.3 darwin/arm64
    
  • Go will automaticly strip of unnecessary code (unused imports, variables, etc.) on saving your file. Keep that in mind.

  • This reduces the code to a minimum

  • Comments in go are declared with //. Multilines comments use /* and */

  • Go is statically typed

  • Go does support concurrency (via go routines) -> using multiple cpu threads

  • If you don't want to use a return value (when multiple are returned like with errors), you can skip them by passing _, eg. _, err := os.Open("something.txt")

  • If you want to return nothing you can return nil (nil is the null known from JS in go)

Casing note

  • Use camelCase for variables names (age, url)

  • Use PascalCames for type names and elements of types (Person, LastName)

  • Exported (public) names are capitalized and other not. So the names of imported packages always use capitalized letters.

  • You cannot access internal (private) names from outside of a package.

Dependencies

  • Dependencies are managed inside of a go.mod file
  • A .mod file does represent a go module. So your entire code could also be treated as a module.
  • So that you don't have to keep on track of your go.mod file manually you can enable the dependency tracking. Then go will do this for you.
  • Enable the dependency tracking by running the following command:
go mod init <REPO-PATH>

example:

go mod init hello/world

Build

  • A build can be created with go build <file>

Packages

  • You can browse through public go packages on pkg.go.dev
  • To import a package add import "URL" to your file
  • With go mod tidy you can install all missing packages
  • With package main a standalone executable is created
  • Its possible to create an alias for an imported module import alias "package"
import f "fmt"

// this is the same
f.Println("Hello world")

// like (would be without an alias)
fmt.Println("Hello world")

Common packages

  • The go standard library includes a wide range/variety of modules
  • fmt is a very common package for formatting text
fmt.Println("something")
  • fmt supports show the type of an variable, converting two different numbers systems and more. See their docs for more info about it.

Variables/constans

  • Declare without value: var name type

  • Declare with value: var name type = "value"

  • Example: var myvar string = "hi"

  • Shortform: name := "value"

  • When using the shortform, go will automatically detect the type which is first initialized

  • Declare multiple vars in one line: name, age := "Alice", 12

  • Outside a function the constructor := isn't available

  • Its also possible to declare constants with the same syntax. Just use the keyword const instead of var.

  • When defining multiple variables with the same type, the type can omit except of the last name before:

var int, y int, z int

after:

var x, y, z int
  • When an variables is declared without an inital value, they will given a zero value, which is something like a default value.
  • The zero values is either 0, false or "" depending on the type of variable (details)

Data types

General

  • bool (true/false)
  • string

Numeric

Type Sign (de: Vorzeichen) Size Possible values
uint unsigned depends on your cpu arch (32 bit / 64 bit)
uint8, byte unsigned 8 bit 0 - 255
uint16 unsigned 16 bit 0 - 65535
uint32 unsigned 32 bit 0 - 4294967295
uint64 unsigned 64 bit 0 - 6551844674407370955161535
uintptr unsigned depends on your cpu arch (32 bit / 64 bit)
int signed depends on your cpu arch (32 bit / 64 bit)
int8 signed 8 bit -128 - 127
int16 signed 16 bit -32768 - 32767
int32, rune signed 32 bit -2147483648 - 2147483647
int64 signed 64 bit -9223372036854775808 - 9223372036854775807
float32 floating-point 32 bit
float64 floating-point 64 bit
complex64 complex numbers
complex128 complex numbers
  • rune is used to declare/represent a single Unicode code point/character, eg. var dash rune = "-"

  • _ can be used inside of numeric datatypes as thousands separator (for easier readability) eg. var money uint32 = 11_468_246

  • It's recommended to use int without a specific size unless having a specific reason (src)

  • Full list

  • Details in easy words

Type conversions

  • You can convert a type by using T(v)
  • Go requires you to explicitly convert types
var i int = 42

// convert int to float32
var f float32 = float32(i)

// the following would fail
var f float32 = i

Data structures

Array

An array is a numbered sequence of elements of a single type.

  • An array has a fixed size, so on declaration you have to provided the size for the array.
  • The declaration has a simple syntax: [size]type{"objects"}
days := [7]string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
  • You can store at maximum as many items in an array, as the specified size of it

Note: Arrays cannot be resized.

  • You can also initialize an empty array
var a [10]int
  • It's possible to edit the values inside an array
days[2] = "Middleday"
fmt.Println(days[2]) // Middleday
fmt.Println(days) // [Monday Tuesday Middleday Thursday Friday Saturday Sunday]

Slice

  • A slice is similiar to an array but with dynamic size
  • A slice does contain elements of a single type
  • A slice is declared with []string, values are declared inside curly brackets {}
  • So its the same syntax like an array but without defining the size
var fruits = []string{"Apple", "Banana", "Mango", "Kiwi"}
  • Getting the length of a slice: len(fruits)

  • If you wan't to get only some range of entries from an array or a slice, you can use a[low:high]

days[1:5] // [Tuesday Wednesday Thursday Friday]
fruits[1:3] // [Banana Mango]
  • When omitting the low or high value, go will use 0 as the default for the low value and the total length of the array as high.
  • So the following expressions/slices are all the same:
  • fruits
  • fruits[0:4]
  • fruits[:4]
  • fruits[0:]
  • fruits[:]

Note: Slices actually don't store any data in reference a array, which stores the data. If a modification is done through a slice, it will therefore modify the contents of the array behind it. This also affects other slices which access the same array.

  • This behaviour gets visible here
var fruits = []string{"Apple", "Banana", "Mango", "Kiwi"}
fmt.Println(fruits) // [Apple Banana Mango Kiwi]

a := fruits[0:2]
b := fruits[1:3]
fmt.Println(a, b) // [Apple Banana] [Banana Mango]

b[0] = "Cherry"
fmt.Println(a, b) // [Apple Cherry] [Cherry Mango]
fmt.Println(fruits) // [Apple Cherry Mango Kiwi]
  • Getting the lenght of a slice: len(fruits)
  • The capactiy of a slice corresponds to the define size of the underlying array
  • It can be accessed via cap(s) eg. cap(fruits)
  • With make it's possible to create a slice whilst specifying the length & capacity of the underlying array (details)

Syntax

make([]type, len, cap)

Example

example := make([]int, 0, 5) // len=0 cap=5 content=[]
  • It is possible to nest slices inside of other slices

  • Also you can append more values to an existing slice (this will automatically create a new array in the background, if the existing one runs out of capacity)

Syntax

append(slice, item1, item2)

Example

fruits = append(fruits, "Cherry") // [Apple Banana Mango Kiwi Cherry]

Map

  • Map = unordered, group of elements of one type
  • A map is a collection of key-value-pairs. It has similiarties with dictionaries, but supports only one type.
  • The type of the keys and values will be fixed on declaration
  • Each entry in a map requires a key
  • Entries in maps are separated by a comma (,). The comma is always at the end, on every line (including the last one).

Syntax

map[key]value

Declaration with values (example)

books := map[string]string{
    "Lying": "978-1-940051-00-0",
    "Atomic Habits": "978-0-593-18964-1",
    "Stop Overthinking": "978-1-64743-249-2",
}
  • Its also possible to declare an empty map with make
map := make(map[string]int)
  • Values can be also assigned by their key later on (or updated the same way)
books["The First 90 Days"] = "978-1-4221-8861-3"
  • To access an values simple call the key from the map
fmt.Println(books["The First 90 Days"])
  • To delete an element use delete(map, key)
delete(books, "The First 90 Days")
  • It's also possible to check wheter a key exists or not
elem, ok := m[key]
// ok is true, when key exists; else false
// elem equals to the value if the key exists

Custom Types

  • A custom type can be declared with the type keyword
  • This can be used for creating aliasses, like byte which is an alias of uint8
type byte uint8

Struct (like a dictionary)

  • Its also possible to create a type out of multiple others with a struct.
  • A struct is something like an object or a collection of multiple fields
  • It consists of multiple keys and values (possibly of different types)
  • In other languages those are known as dictionary/dict
type Person struct {
    Name string
    Age byte
    Student bool
}
  • To declare a variable with a struct as value, the syntax is TypeName{ TypeA: "value", TypeB: "value"}
var personA Person = Person{
    Name: "Max",
    Age: 20,
    Student: true,
}
  • Because the fields inside a struct. are defined, its possible to omit them on declaration
var personB Person = Person{"Lisa", 19, false}
  • It's possible to access the values of a struct via a dot (like in JSON)
personA.Name
fmt.Println(personA.Name, personB.Age)
  • Direct access to values inside of structs is possible via pointers via (*p).X, eg. (*p).Name

  • But because this cumbersome it's also possible to skip the parenthesis and write p.X to create a pointer.

  • Custom types/structs can also be nested inside of each other

Interfaces

  • Interfaces are more a structural thing than business logic
  • It's like a contract between a function & type (specifically a struct)
  • They define some things, which will be implemented by structs & functions
  • As example you define a interface which specifies which type a function returns; then you define a function which connects a struct type to the created interface
  • Such a connector function could look like this:

Interface Example Image source

type Interface interface {
    Function() ReturnType
}

type Struct struct {
    value ReturnType
}

func (s Struct) Function() ReturnType {
    // some logic for connecting Function() with s
    return s.value
}

func letItHappen(i Interface) ReturnType {
    return i.Function()
}
  • A interface enables you to connect multiple types together to create logical relations

  • For instance you could create a Shape interfaces which has a logical relation to Rectangle or Circle

  • When you go more advanced, it's also possible to nest multiple interfaces into each other and so create bigger constructs

  • But the principle stays the same

  • See this video for better unterstanding of the concept


  • An interface with zero methods in it is called "empty interface"
interface{}
  • It can be used to handle code which takes any number of arguments of a unknown type
  • To pass a argument via an empty interface add parentheses with the value at the end of it
interface{}(10)
  • You can get the type of an interface by doing .(type) on it

  • Interfaces are often named with an suffix er after their function name

Examples:

  • Abs() -> Abser
  • String() -> Stringer
  • etc.

Stringer

  • Stringer is a very common interface which describes itself as a string (used by fmt)
  • It has a simple structure
type Stringer interface {
    String() string
}
  • Stringer is used to declare a custom formatting for strings (and overwrite the default formatting %v of fmt)
  • The fmt packages does run the String() function as method on the string to format it
  • So it's really no magic what it does

Definition of a cusotm String() function

func (t T) String() string {
	return fmt.Sprintf("your formatting", t)
}
  • As example to create a custom formatting for IPv4 addresses:
package main

import "fmt"

type IPAddr [4]byte

func (ip IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

The result before was (without the func String()):

loopback: [127 0 0 1]
googleDNS: [8 8 8 8]

And afterwards:

loopback: 127.0.0.1
googleDNS: 8.8.8.8

Generics

  • Generics can be used if a function should work for different types
  • To enable generics for a function add [T] before the argument definition
  • Sometimes you require additional features like the ability to compare values; in such case a the corresponding constraint after the T declaration
  • Then you can declare parameters of the type T, which all have the same type, but no matter which type
func compare[T comparable](a T, b T) bool{
    if a == b {
        return true
    }
    return false
}
  • It's also possible to use generics for type declaration in addition to functions

  • You can create own constraints for your generic functions with interfaces

  • You also can allow the usage of inherited/underlying types with ~ in front of the type name

  • Eg. ~int allows also custom integer-based types like age (type age int)

type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64
}

Usage:

func duplicate[T Number](count T) T {
    return count * 2
}

Functions

func myFunc(a rune, b rune) rune {
    return a * b
}
  • Functions with generics instead of specified types are also possible

  • Use a letter like T for its initialization

  • Go does support functions with multiple return values

func myFunc(a string, b int) (string, int) {
    return a, b
}
  • Its also possible to return named values/variables (without passing via the return keyword)
func myFunc(x int, y int) (z int) {
	z = x * y
	return
}

fmt.Println(myFunc(2, 3))
  • This is also called a "naked" return (src)

Function values & closures

  • Functions can also be assigend to a variable and than access via those variable name; in this case a function doesn't need a own name
  • This is called a function value or a anonymous function
multi := func(x, y int) int {
    return x * y
}
  • Therefore it's also possible to use a function (which was defined on the fly) as return value for any other function
func fibonacci() func() int { // func() int -> is the return value of the function
	return func() int{ // therefore it needs to be returned
		return 1 // and because it's a function, those also have a return value their
	}
}
  • It's even possible to reference variables from outside the function when using a anonymous function.
  • This is called a "closure"

A closure is a function value that references variables from outside its body. The function may access and assign to the referenced variables; in this sense the function is "bound" to the variables. (Src: A Tour of Go)

func fibonacci() func() int {
	first, second := 0, 1 // runs only once; there value will be stored inside `f` in main()
	return func() int{
		curr := first
		first, second = second, first+second
		return curr
		// 1th round: first: 1, second: 2: curr: 0
		// 2th round: first: 2, second: 3: curr: 1
		// 3th round: first: 3, second: 5: curr: 2
		// and so on
	}
}

func main() {
	f := fibonacci() // stores the values of first, second
	// inside of `f` the environment (values first & second) of the body of fibonacci() is stored
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}
  • So you define a function as return value of another function (fibonacci())

  • Then you define some variables outside of this return function (first, second)

  • If you now call the function (fibonacci()) within another variable (f) the values of the variables inside the outer function (first, second) were only set once on initialization

  • On further calls of the variable (f) as function call (like f()) the values don't get touched on just the anonymous function is run

  • Tutorial for better unterstanding: A Tour of Go: Fibonacci Closure Exercise

  • Where do you use first class functions? - r/golang

Methods

  • It's also possible to define a function as method, so it can be accessed via .myFunc() instead of myFunc()

  • A method consists of a receiver argument, which is the item on which the method is executed.

  • Receiver argument means the value, on which the function get's executed

  • So in the following argument distance is the receiver argument, which gets passed to Kilometers()

distance.Kilometers()
  • To create a method (from a function) move the receiver argument before the declaration of the function name
type Name string

func (n Name) Greet() {
	fmt.Printf("Hello %s!", n)
}
  • Now this method can be access on other items/objects of the right type.
func main() {
	var mike Name = "Mike"
	mike.Greet()
}
  • Methods can only be created with receivers for custom types which are declared inside the same package as the method (src)

  • It's not possible to use a built-in type like int or string as receiver

  • A simple workaround is to define a custom type like type Workaround string inside the package

  • By default methods don't modify the original received argument and instead create a copy of it

  • However if you want to convert the receiver, this isn't the solution

  • To address this issue, you can declare the receiver argument as pointer (via asterisk *).

  • This will enable you to directly access and edit the original receiver argument

  • Methods accepts both pointers and values even if you don't specify so; function on the other hand strictly require the type of allocation which was defined (details)

Example (src)

func (v Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}
// --> won't modify the receiver's value
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}
// --> will modify the receiver's value
  • If you require a pointer as receiver and don't assign one, it's possible that go does automatically call it as pointer. It depends. That's only the case with methods. Functions require a pointer, if defined so (details)
  • Most often you should use a pointer with methods, even if you don't modify the value. It can be more efficient when working with large structs.
  • No matter if you use pointers or not, you should use the same way to access thoose types via methods all the time (no mixes of value copying and pointers) to prevent issues.

init() function

  • The init() function is a special function in go, which is executed on initialization of a package
  • It's run before the main() func is run
  • Also it's only run once
func init() {
    // something
}

Errors

  • The error handling in go is based on interfaces & structs (similar to stringer)
  • When you take a look inside the source code of go, you will find the following interface definition:
type error interface {
    Error() string
}
  • The individual error types are defined as struct

Example:

type CalculationError struct {
    msg string
}
  • On error the function Error() get's called, which is connect to the error interface
  • So for each error type a corresponding Error() function is required
func (ce CalculationError) Error() string {
    return ce.msg
}
  • By creating those two definitions (error type as struct, implementation of a Error() function) you can create custom error types which let your code compile

Compute/detect errors

  • Most functions return two types: the expected value + an error type
  • This makes it easy to catch errors and react to it
  • If no error occured, the value of the error type simply is nil
  • Before processing the value got from a function, you should check, if a error happend
if err != nil {
	// do something after a error happend
}

Throwing custom errors

  • As you seen before, most functions return errors as a seperate return parameter
  • Do the same in your custom functions

Example

func performCalculation(a int) (int, error) {
    if a < 0 {
        return 0, CalculationError{
            msg: "Invalid value"
        }
    }
    return a * a
}

Other notes

  • Maybe you want to log a custom message to stderr
  • You can do this by using log.Fatal()
log.Fatal(err)

Docs errors pkg A Tour of Go

Control flows

Conditionals

  • The if statement in go are as you would expect it
  • The usage uf parentheses () (for declaring the condition) is optional.
  • However the curly brackets {} for the actual code is required. So no inline if/else is possible.
x := 0

if x > 10 {
    fmt.Println("hello")
} else if x > 5 {
    fmt.Println("world")
} else {
    fmt.Println("bye")
}
  • It's also possible to define an variable inside an if statement.
  • Such variables are only accessable (scoped) inside the statement himself.
if x := 0; x > 10 {
    // do something (it's possible to use x)
}
// x isn't accessable here (outside the statement)

Switch case

  • Go also supports switch statements, which is very useful
score := 25

switch score {
    case 100:
        fmt.Println("Crazy, you're the best")
    case 75:
        fmt.Println("Pretty much it")
    case 50:
        fmt.Println("Hey, not bad.")
    default:
        fmt.Println("Nice try")
}
  • In go the switch case stops, when it finds a match (unlike other languages which require break to stop execution)
  • With fallthrough you can declare that when one condition is met, the next block is also run
score := 25

switch score {
    case 100:
        fmt.Println("Crazy, you're the best")
    case 75:
        fmt.Println("Pretty much it")
    case 50:
        fallthrough
    default:
        fmt.Println("Try again")
}
  • You can also declare a var inline of the switch statement
switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("You are a mac user")
case "linux":
    fmt.Println("Hi linux guy")
default:
    fmt.Println("I don't what user you are")
}
  • Switch can also be used for checking against individual conditions for each case (like switch true in other languages)
  • Just write switch without a condition and you're good to go
t := time.Now()
switch {
    case t.Hour() < 12:
        fmt.Println("Good morning")
    case t.Hour() > 18:
        fmt.Println("Good night")
    default:
        fmt.Println("Good day")
}

For loop

  • The syntax of a for loop is very simple and as you would expect it Syntax:
for init; condition; post {
    // task
}
  • The loop is run aslong as the condition meets true

Example:

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

While

  • Go only has for loops. There is no where. But it's possible to create something similar using for loops. Example:
counter := 1

for counter < 1000 {
    fmt.Println(counter)
    counter = counter * 2
}

Range / for range / for in

  • Its also possible to range over items (eg. slice) with a for loop. As an for range:
var fruits = []string{"Apple", "Banana", "Mango", "Kiwi"}

for i, v := range fruits {
    fmt.Printf("current index: %d, current value: %s\n", i, v)
}
  • You can access the index & a copy of the current value through custom variables (as seen in the example above)

Infinite loop

  • Its also possible to create an infinite loop with for
  • Just don't declare anything after the for keyword and here you go (haha, go)
for {
    fmt.Println("just another line")
}

Defer

  • With defer it is possible to defer the execution of a function untill the rest of the current function is executed
  • So a defered task is run at the end of the function/the last step
func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}
  • The advantage of this is that you can define a task in advance that will be executed at the end, without risking forgetting it (eg. closing an open file)
  • When you define multiple defered tasks/steps, then the execution order is as follows
  1. normal steps
  2. defer task that was defined last
  3. defer task that was defined second last
  4. derfer task that was defined XX. last
  5. defer task that was defined first

Pointers

  • Go does have pointers

  • A pointer is a way to reference and modifiy the value (technically the memory address of the variable) of a variable through another (reference)

  • The type of a pointer can be defined with an asterisk * before the type's name.

var p *int
  • Use the ampersand & to directly point to a variable
i := 42
p = &i
  • To change the value via an pointer, also use an asterisk *
*p = 21
  • Pointers are useful because often variables are only passed as copy to a function and therefore not modified in their origin place

  • To ensure you modify the original var, you can use pointers (lab)

  • More details/src

Testing

  • Testing is a must every application to ensure it behaves like expected
  • It also always you to gurantee functionality over team whilst the code changes
  • It makes you develop and validate functionality faster
  • You now longer have to check functionality after every change manually

Types of tests

  • There are different types of tests (in general; not related to go)
Type Definition
Unit Test Tests the functionality of individual components independantly
Integration Test Tests the functionality of multiple components working together
Smoke Test A quick, high-level test of the basic features before going more deep with other test types
E2E Test Test of the complete application from one end to the other; from user to server

Unit tests

  • Inside a unit test you should test the expected behaviour and not how the result was achieved (this would prevent you from refactoring your code)

  • You should write simple and small tests the each test one specific func instead of big tests which test everything in one

  • This allows for better traceability

  • You should continuously write tests while developing for every new func and not wait until you developed already the entire application

  • Else it's very overwhelming to create tests and you may be miss something

  • In addition you may notice that you have to refactor some code, which you else had detected way earlier

  • According to "Learn Go with tests" it's even better to write the tests, before writing your func. So you think of what your func should do/deliver before writing it.

  • So the func's requirements can be defined first and then the actual code to get there is implemented.

  • The advantage of this is that it ensures the function itself—the one it is supposed to perform—is tested, rather than something else that might skew the results.

  • Go has built-in unit testing tools unlike other languages

  • To declare a file in go as "test definition" the name must end with _test eg. fibonacci_test.go

  • It's best practice to save the test file in the same directory like the function which will be tested and name the file the same except with the _test suffix added.

  • Each test bases on three pieces: given, when, then

  • Their self explaining:

    • given: conditions/requirements/test infra
    • when: steps which will be executed on testing
    • then: expected result (outcome) of the test
  • You can form the variables got (result) and want (expected) out of this

  • To declare a new test create a func with the same name like to function to test but with the prefix Test added.

  • Go automatically detects functions with the prefix Test as unit test. So don't name your regular functions with a prefix Test when they aren't tests

  • Example: GetAllProjects() -> TestGetAllProjects()

  • You also need to declare a input argument of the type *testing.T usually named t like t *testing.T

  • Example: func TestGetAllProjects(t *testing.T) {}

  • This is the only argument the function takes

  • Inside the func you should declare the steps to test your function and which results you expect

  • In the end of your function you should throw an error with t.Errorf() if the test didn't passed

  • Often you declare the test environments through a dedicated struct and the define multiple scenarios which you'il store inside a slice of this struct

  • Then you easily can loop over all scenarios

  • This is called "table driven tests"

Example of a simple test:

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5

    if got != want {
        t.Errorf("Add(2, 3) failed: got %d, want %d", got, want)
    }
}

Example of a table driven test:

func TestAdd(t *testing.T) {
    tests := []struct{
        a int
        b int
        want int
    }{
        {2, 3, 5},
        {1, 4, 5},
        {7, 9, 16},
        {4, 8, 12},
    }

    for i, tt := range tests {
        t.Run(i, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) failed: got %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}
  • It's also possible to define the test cases using a map. This would allow you to store the name of the test as key inside the map.
tests := map[string]struct {
  input string
  result string
} {
  "empty string":  {
    input: "",
    result: "",
  },
  "one character": {
    input: "x",
    result: "x",
  },
  "one multi byte glyph": {
    input: "🎉",
    result: "🎉",
  },
  "string with multiple multi-byte glyphs": {
    input: "🥳🎉🐶",
    result: "🐶🎉🥳",
  },
}

(example from)

  • The tests can be run with a simple command
go test
  • By default only the result of failed tests is shown
  • To see the results of all the individual tests add the flag -v (verbose) before running
go test -v
  • If you want to check how much of your project you test, you can run the following:
go test -v -coverprofile cover.out
  • At the end of the output you see how many lines of code in your project were tested (coverage)
  • To see which lines were not covered by tests, you can view the output of the command before as HTML
go tool cover -html=cover.out

Sub tests

  • Inside a test it's possible to split it up into two or more "subtests"
  • To do this, use t.Run("title", func(t * testing.T) { test here... })

Benchmarks

Sources/Resources