Go - kamialie/knowledge_corner GitHub Wiki

Go

Hello world:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello world!")
}

Language characteristics:

  • Strong, static type system
  • C-inspired syntax
  • Multi-paradigm (procedural and object-oriented)
  • Garbage collector
  • Fully compiled
  • Rapid compilation
  • Single binary (however, intermediate libraries, plug-in systems are available)

Use cases:

  • Web services (moving data between servers)
  • Web applications (end user, html, etc)
  • DevOps space
  • GUI
  • Machine learning

Semi-colons are automatically added by compiler to the end of lines that do not end with a symbol.

Data types

Go is statically-typed language. Variables can be declared explicitly or use implicit initialization syntax (compiler determines the type).

Simple types

Go builtin - about simple types and more.

Declaring a variable:

package main

func main() {
    // Verbose
    var i int
    i = 42

    // One line
    var f float32 = 3.14

    // Implicit initialization syntax
    var name = "Mike"
    // Short declaration syntax ":="
    firstName := "Name"

    // Complex type
    c := complex(3, 4)
    // Split real and imaginary parts; similarly declare multiple variables at
    // once
    r, i := real(c), imag(c)
    a, b := 3, 5

    // Variable block (does not require implicit declaration syntax `:=`):
    var(
        index = 1
    )
}

Dividing integers always is performed as integer division (remainder is dropped); the result of a floating number division is a decimal number.

Strings

String types:

  • "example" (with quotes) - interpreted string, handles escape characters as well
  • `example` (backticks) - raw string

Numbers

Numeric types include integers, unsigned integers, floating point numbers, and complex numbers. Common examples are int, uint, float32 and float64, complex64, complex128 respectively.


Boolean

Simply true or false value.


Error types

Conventional interface for representing an error condition, with the nil value representing no error. Represents almost any type or data type that has an error method, meaning it can report what the actual error was.

Pointer

Uninitialized pointer equals nil. Value can not be assigned through uninitialized pointer, thus, it should be initialize with new() function or getting an address of a pre-existing variable.

package main

import "fmt"

func main() {
    var firstName *string = new(string)
    firstName = "Name"

    foo := "bar"
    ptr := &foo
    *ptr = "updated_bar"
    fmt.Println(ptr, *ptr)
}

Constant

Constant must be declared at the time as initialization (same line). Value type must be known at compile time. Implicitly typed constant (type is reevaluated for each case) can be also declared on a package level (outside of any function): either standalone or as const block.

Implicitly typed declaration marks a variable as a literal, which allows it to be used in other contexts, e.g. assigning integer constant to floating point variable type. Explicitly typed constant enforces that variable to be used only when that specific type is allowed to be used.

Uninitialized constant gets the constant expression of a previous constant, e.g. 2 * 5.

package main

import "fmt"

const(
    foo = "bar"
    b // "bar"
)

const pi = 3.14

func main() {
    const pi float32 = 3.14

    const c = 3
    fmt.Println(c + 2)
    fmt.Println(c + 1.5)
}

iota

Used only in the context of constant variables. Represents an expression, not a literal value, which gets incremented on position within a block. Can also be used in expressions. The value of iota resets with a new constant block.

package main

const(
    a = iota     // 0
    b            // 1
    c = 3 * iota // 6
)

const(
    d = iota // 0
)

const(
    e = 5
    f = iota // 1
)

Aggregate data types

Array

Fixed sized collection. All items must be of the same type. 2 arrays can be compared, using == operator, - first, length and type are compared, then each item is compared for equality.

var arr [3]int
arr[0] = 1

implicit_arr := [3]int{1, 2, 3}

fmt.Println(len(arr))

// Arrays are copied by value
// arr2 is a distinct array with exactly same value
arr2 := arr

Slice

Dynamically sized collection (reference type). Doesn't actually hold its own data, but rather refers to underlying array (if data changes in that array, it is also reflected in the slice as well). 2 slices can not be compared.

// As opposed to array, no need to specify size
slice := []int{1, 2, 3}

// Add elements to the slice, put slice itself as first argument
slice = append(slice, 4)
// Remove elements from the slice; remove indices 1 and 2
// Requires experimental slices package as of 1.19
slice = slices.Delete(slice, 1, 3)

# starting from 1st element
s2 := slice[1:]
# up to but not including
s3 := slice[:2]
# starting and ending indexes
s4 := slice[1:2]

arr := [3]int{1, 2, 3}

# slice operator
# any changes to array are reflected in a slice and vice versa
# slice is sort of pointing to the array
slice := arr[:]

Map

Similar to slice, map is a reference type - actual data is stored elsewhere. 2 maps can not be compared.

# Key of type string, value of type int
# All keys and all values should be of the same type as defined
m := map[string]int{"foo": 42}
fmt.Println(m["foo"])

// Update existing item or create new
m["bar"] = 21

delete(m, "foo")

Queries always return a result; for non-existing key the default value for the type is returned, e.g. 0 for int. Comma ok syntax can be used to determine, if a value came from the map or not. ok is just a conventional name for variable in this context; is of type boolean.

v, ok := m["foo"]

Struct

Fields can be any of types, but are fixed at compile time. Declaration and definition must be separate. Each field is initialized to zero value. Struct is a value type, like arrays, which means assignment operation creates a separate value copy.

// Anonymous structure
var s struct {
    name string
}

// Custom structure
type user struct {
	ID int
	firstName string
	lastName string
}

var u user
u.ID = 1
u.firstName = "Alex"

u2 := user{ID: 1, firstName: "Alex", lastName: "Dot"}

Code structure

Function

Parameters

Function parameters can stay unused - this won't cause compile error. Parameters of the same type can be listed under single type as a list.

func function_name(param1 int, param2 string) {
}

func function_name(param1, param2 int) {
}

func function_name(param1, param2 int) bool {
	return true
}

Variadic parameter transforms the type to a collection. Within a function acts as a slice. Can only specify one variadic parameter and can only be specified last. Function call accepts multiple values separated by comma.

func print_names(names ...string) {
    for _, n : range names {
        println(n)
    }
}
// print_names("Alex", "Max", "Sam")

Return value

Return value is optionally set. Multiple value can be returned. Common practice is to return a value alongside error (pointer type) in case function encounters a problem. To ignore a return value use write only variable. import "errors"

func function_name(param1, param2 int) error {
	return errors.New("smth went wrong")
}

func function_name(param1, param2 int) (int, error) {
	return 1, nill
}

// write only variable
_, err = function_name()

Return values can also be named; in this case also a naked return statement can be used, normal return statement can still be used as well (rarely used):

func function_name(param1, param2 int) (result int, status error) {
    result := 1
    var status error = nil
	return
}

Package

A package is a directory located in the same space as go.mod, which contains collection of source files that are compiled together (at least one file). Packages can also be nested - another directory within a package that also has at least one source file is also a package (subpackage).

Any source file starts with a package <value> statement, package declaration. Either main or name of directory that contains the package.

Visibility mode

Functions, types, variables, and constants defined in sources files are members of a package.

There are 2 visibility modes: package and public. Visibility mode determines what importing code can see from a package. Package mode makes all members visible within a package by default. Capitalized members, e.g. type User struct have a public visibility, become part of public API of a package. Public members are visible for in imported packages. Same applies to structure members.


Package identifier

Name of directory becomes the name of the package. Package can be referenced from the root package by appending it to path after slash.

Fully Qualified Package Name is used for imports; check also identifiers. To import a bar package in the root package in a module named foo (look in go.mod file) the following identified is used - foo/bar.

Module

A module is a collection of related Go packages that are released together.

Documentation

Comments follow C notation:

// single line comment
var i // same line comment

/*
multiline comment
*/

Documentation is expected in certain places of any Go program. First, explain what package provides and what it is used for. Start with Package .

// Package user provides ...
package user

Any public members of a package are also expected to be documented, such as variables and functions. Start with the name of a member, e.g. GetByID searches...:

// MaxUsers controls how many user can be handled at once.
const MaxUsers = 100

// GetByID searches for users using their employee ID.
func GetByID(id int) (User, bool) {}

Program flow

If statements. Initilizer can also be optionally specified; valid only on first if clause, if multiple branches are also specified.

i := 1
j := 2

if i == j {
	println("equal")
} else if i < 2 {
	println("less")
} else {
	println("more")
}

if k := 1, k == 1 {
    println("if with initializer")
}

Switch statements. Break is implicit. Multiple case expression can be specified. Initializer syntax can be used here as well, similarly to if statements.

method := "POST"
switch method {
case "POST":
    println("POST")
case "GET", var:
    println("GET")
    fallthrough
case "PUT":
    println("PUT")
default:
    println("default")
}

Logical switch is the one that has test expression set to true, so each case checks if it evaluates to true. Since this is a common use case, true can be omitted, in which case it is implied.

switch i := 2, true {
case i < 5:
    println("i is less that 5")
case i < 10:
    println("i is less that 10")
default:
    println("i is greater or equal to 10")
}

Loops

Basic loops:

  • infinite loop
     var i int
     for {
     	if i == 5 {
     		break
     	}
     	println(i)
     	i += 1
     }
  • loop till condition
     var i int
     for i < 5 {
     	if i == 1 {
     		continue
     	}
     	if i == 3 {
     		break
     	}
     	i += 1
     }
  • loop till condition with post clause
     for i := 0; i < 5; i++ {
     	...
     }

Looping over collections structure follows the form for key, value := range collection { ... }. For slice and array a key is an index.

slice := []int{1, 2, 3}
// range keyword tells that the following identifier represents
// a collection, and returns key/value pair on each iteration
for i, v := range slice {
    println(i, v)
}

m := map[string]int{"one": 1, "two": 2}
for k, v := range m {
    println(k, v)
}
# Retrieve only keys
for k := range m {
    println(k
}
# Retrieve only values using blank identifier
for _, v := range m {
    println(v
}

Deferred function

Perform additional statements after function exit and before returning focus to the called. Deferred statements are identified by defer keyword, and are executed in the opposite order - last one registered is executed first.

func main() {
    println("main 1")
    defer println("defer 1")
    println("main 2")
    defer println("defer 2")
}

/*
main 1
main 1
defer 2
defer 1
*/

Panic

panic keyword immediately stops the execution of the current function and returns focus to the called. If no recovery steps are present, this repeats until program ultimately exits. Any type of data can be passed to a panic function, e.g. string, slice, object.

func example() {
    println("One")
    panic("Error message")
    println("Two")
}

Recover is done with defer concept and recover function. The latter returns everything that was passed to panic. Once recover function is called, Go assumes that panic was handled and continues normal execution of the calling function.

func example() {
    defer func() {
        fmt.Prinln(recover())
    }()
    fmt.Prinln("One")
    panic("Error message")
    fmt.Prinln("Two")
}

Anonymous function executes even if no panic occurred. return function return nil, if panic has no occurred, which can be used in an if statement to perform recovery steps or not.

Goto

func example() {
    i := 10
    if i < 15 {
        goto my_label
    }
my_label:
    j := 42
    for ; i < 15; i++ {
        ...
    }
}

goto statement in Go follows 4 rules:

  1. Can leave a block, e.g. if statement, for loop
  2. Can jump to a containing block, e.g. function
  3. Can no jump after variable declaration - if variable was not declared as part of normal sequential execution, it will not be known.
  4. Can not jump directly into another block, e.g. any block at the same level as the block that was left.

Object orientation and polymorphism

Method

Multiple functions can be grouped via struct, thus, becoming methods. Method needs a type to bind to. A method receiver is between func and name of the method. Thus, methods represents a tight coupling between a function and a type - method only works in the context of a specific type.

type someStruct struct {}

func (ss someStruct) func_name(param1 int, param2 string) {}

var newStruct someStruct
result = newStruct.func_name(35, "string")

Method receiver

Receiver could be of type value or pointer. When data has to be shared between the caller and a method, a pointer should be used.

type user struct {
  id    int
  name  string
}

func (u user) String() string {
  return fmt.Sprintf(..)
}

func (u *user) UpdateName(n name) {
    u.name = name
}

Interface

Similar to struct, but contains methods that must be implemented. Once an interface variable is created, and particular type is assigned to it, only interface methods are visible.

type Reader interface {
  Read([]byte) (int, error)
}

type File struct {...}
func (f File) Read(b []byte) (n int, err error)

type TCPConn struct {...}
func (t TCPConn) Read(b []byte) (n int, err error)

var f File
var t TCPConn

var r Reader
r = f
r.Read(...)

r = t
r.Read(...)
var f File
var r Reader = f
// A compile time error, since Go doesn't know the underlying type of an
// interface in general.
var f2 File = r

// Type assertion - tell explicitly what type does the variable contain. If
wrong, a runtime panic occurs.
f2 = r.(File)
// Safe type assertion
f2, ok := r.(File)

To test an interface for multiple underlying types use type switch:

var f File
var r Reader = f

switch v := r.(type) {
case File:
  // v is now a File object
case TCPConn:
  // v is now a TCPConn object
default:
  // if v is none of the objects types above
}

Generic

Interface loses the original identity of an object, thus, requiring type assertion to be able to access all other properties of a given object. Generic function allows to temporary treat an object to act as another type and return to its original form outside this function.

Generic type is defined after a function name, but before parenthesis for parameter list. any is a built-in constraint, which represents an interface with 0 or more methods.

//Accepts any time of slice
func clone[V any](s []V) []V { ... }

For generic function that deals with maps any can not be used for the key, as it has to be some type that can be compared; comparable represent such types.

//
func clone[K comparable, V any](m map[K]V) map[K]V { ... }

Custom type constraint

Some operations, such as addition, can not be performed for all types, any. Thus, an explicit interface must be created, where each type is listed, to allow Go to check, if that type can performed certain actions. Adding boolean to the interface will cause an error.

type addable interface {
  int | float64
}

func add[V ](s []V) V {
  var result V
  ...
    result += V
}

Constraints package contains more types that are not built-in.

Standard library

net and net/http

package main

import(
    "io"
    "net/http"
    "os"
)

func main() {
    http.HandleFunc("/", Handler)
    http.ListenAndServe(":3000", nil)

    func Handler(w http.ResponseWriter, r *http.Request) {
        f, _ := io.Open("./file.txt")
        io.Copy(w, f)
    }
}

os, fmt, bufio

package main

import(
    "bufio"
    "fmt"
)

func main() {
    fmt.Println("Enter text")

    // Wrap raw Stdin with reader decorator
    in := bufio.NewReader(os.Stdin)
}

Sort out

var = flag.String(name, default_value, description)

flag.Parse()
package main

import (
    "log"
    "net/http"
)

func main() {
    http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, world!"))
    })

    err := http.ListenAndServe(":3000", nil)

    if err != nil {
        log.Fatal(err)
    }
}

CLI

# initialise project
$ go mod init

# run single file
$ go run main.go

go env - prints available env vars, can also modify, unset and add more go fmt (calls gofmt standalone) - applies best practices for formatting source code.

go get - get (download and install) packages (also modifies go.mod) go list -m all - list all packages

go doc - prints the documentation of the package or symbols inside it

Build and run

Go run command builds and runs an application in place without leaving a binary on disk. Provide path to the source file as a command parameter. Imported dependencies must be included in the

Go build compiles the packages - binaries are saved, but libraries are discarded by default. However, this behavior can be changed with various flags.

# save library after build
$ go build -a package_name.a package_name

# save intermediate dependecies (pkg directory)
$ go build -i binary_name

IDE

VSCode

Shift + CMD + P -> Go: Install/Update Tools (needs gopls first - language server)

  1. go mod init

Debugging

To be able to pass input to the program and debug it add the following to launch configuration:

{
    "configurations": [
        "console": "integratedTerminal"
    ]
}

Frameworks

Network services

Go kit - comprehensive microservice framework

Gin - fast, lightweight web framework

Gorilla Toolkit - collection of useful tools

CLI

Cobra - framework for building CLI apps

Resources


⚠️ **GitHub.com Fallback** ⚠️