Go Coding One Pager
I’m heavily indebted to the Pluralsight Go courses and to Jon Bodner’s Learning Go: An Idiomatic Approach to Real-World Go Programming. This is not a complete cheatsheet. For example, I won’t mention small stuff like unused constants causing a compile-time error. The full Go language specification is located at https://go.dev/ref/spec.A. First Steps
A.1 Create the Module
A.1.1 vim, neovim, vi, emacs
If Go is installed, just create a project directory and execute:
cd <project directory>
go mod init <repo url>/<teamname>/<project name> # i.e., the module
For example:
go mod init github.com/PentheusLennuye/go_cheatsheet
This creates go.mod, which is used to identify the module and to lock dependency versions.
A.1.2 Visual Studio Code
If using Visual Studio Code, copy the template created in Docker Visual Studio Code section B, open the folder in Visual Studio Code, and then initialize the project as a Go module. For example, if the project is called Canasta:
cp <path/to/go template> canasta
cd canasta
code . # Starts MS Visual Studio Code
Inside VS Code terminal:
vscode@whatever/workspaces/canasta $ go mod init github.com/<path/to/>canasta
A.2 The Makefile
Here is a dirt simple Makefile for the root directory of your module (in Go, a project is called a module).
.DEFAULT_GOAL := build
.PHONY:fmt
fmt:
go fmt ./...
.PHONY:build
build: fmt
go build -o bin/ cmd/hello_word/hello_world.go
On the command make build
or even just make
, Make sees that it needs to run
the “fmt” target as a prerequisite to the “build” target.
This assumes that there is a source file called hello_world.go in the cmd/hellow_world/ directory. The “-o bin/” flag pushes the compiled code into a directory which, if .gitignore is properly set, will keep binaries out of your source repository. This won’t matter so much in Windows as binaries have the .exe, .dll, .so extensions which can also be filtered by .gitignore. However, Mac and Linux binaries generally have no extensions at all.
A.3 Module Structure
In Python, projects contain modules. In Go, however, modules contain packages. Python project = Go module. Python module = Go package.
Jon Bodner suggests that small modules can be kept in just a root directory to avoid the unnecessary complication of package directories.
However, as the project expands …
|- bin/ ①
|- cmd/ ②
| |- hello_world/
| | |- hello_world.go
| |- k8s_user_add/
| | |- k8s_user_add.go
|- internal/ ③
| | |- libA.go
|- pkg/
| |- <package name A>/ ④
| | |- source_file_A.go
| | |- source_file_B.go
| |- <package name B>/
| | |- source_file_C.go
| | |- source_file_D.go
| |- internal/ ⑤
| | |- libB.go
...
➀ In Linux, binaries have no extensions, so place them in bin/ to avoid being
checked into git.
➁ Source code for executable. All files here start with
package main
➂ Shared identifiers that are not part of public API but are available to
cmd/, pkg/ and the root directory. All files here start with
package internal
. They can be imported with
import "<URL>/internal"
➃ All files here start with package <package name A>
, This is not strictly
necessary but it is recommended.
➄ Similarly to ➂, these shared identifiers are not part of public
API but rather to pkg/, package name A/, package name B/. Use import "<URL>/pkg/internal"
.
- Package names do not need to reflect the directory name. However, common convention means the folder name should be the same as the package name. Exception: cmd/ files should all be package “main”.
- Strive to limit dependencies between packages.
A.4 Importing Packages Into Code
Almost all starting Go coders will use the standard library “fmt” package. Here is a hello world program with the path, cmd/hello_world.go.
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
Say that hello world needs to use local packages and third-party crypto packages. Say also that the module URL is “github.com/PentheusLennuye/foo”. The hello_world.go would start with:
package main
import (
crand "crypto/rand" // ❶
"fmt"
"math/rand"
"github.com/PentheusLennuye/foo/pkg/directoryA" // ❷
"github.com/PentheusLennuye/foo/pkg/directoryB"
)
... // code here
➀ The module crypto/rand is renamed internally to crand to prevent a
collision with math/rand later on.
➁ Imports use the file path, not the package name.
- If nothing is used from fmt, directoryA, or directoryB, a compile-time error occurs.
- The third-party packages will be downloaded, a line in go.mod’s require()
block added, and checksums placed in go.sum by the
go get <url>
command.
A.5 Promise Not to Use the init() Function
Do not use the init() function for your packages. It is used to initialize package-level variables (i.e. global variables) when the package is first referred to. Mutable global variables are Bad(tm). However, if using concurrent goroutines on I/O, init() may be used for lazy initialization.
B. Styles and Idioms
Some of these idioms will need to be explained lower down. These idioms are up here because once one has learned the language, one wants to find the idioms first.
B.1 The semicolon insertion rule
Go requires a semi-colon after every line. In fact, Go inserts it itself so the developers do not have to. However, that means it can get confused if the developer ends a line with an identifier, literal, certain tokens, and “+”, “-”, or “}”.
This is incorrect:
func main()
{
...
}
This is correct:
func main() {
...
}
B.2 The Comma OK Idiom
Sometimes a zero returned from a function is not a zero. Perhaps the zero was returned because the function really wanted to send a null but could not due to the return type. In this case, use the “comma ok” idiom. Go does not have a try/catch/exception channel.
var m int
if m, ok = fooie("Looie"); ok {
// "Looie" was accepted, deal with 'm'
} else {
// "Looie" is a problem. Deal with the problem
}
func fooie (string argumentString) (int, bool) {
...
return whatever, true
}
B.3 The Error Idiom
Idiomatic Go avoids nested blocks. Go also has no Try/Catch/Exception channel since its designers want just one data flow between functions. Go programmers use the idiom of always sending an Error or a nil as the last return value for a function.
func myFunction() (int, error) {
...
if <terrible thing a happens> {
return 0, error.New("terrible thing a") // ❶
}
if <terrible thing b happens. {
return 0, fmt.Errorf("terrible thing %s", b)
}
// A successful function returns nil instead of Error
return myResult, nil
}
result, err := myFunction()
if err != nil {
... // process the error, including passing it up
}
... // continue on your merry way
➀ By convention, error messages use no capitals or punctuation
B.4 Go Pattern for file access
func getFile(path string) (*os.File, func(), error) {
file, err := os.Open(path)
if err != nil {
return nil, nil, err
}
return file, func() { file.Close() }, nil
}
f, closer, err := getFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer closer() // ①
➀ defer is a statement that ensures its argument function is executed just before the function returns.
B.5 The Slice As Buffer Idiom
Go file buffers are more efficient than other languages, as its slice type can be reused rather than reallocated. Using getFile() from B.4 …
f, closer, err := getFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer closer()
data := make([]byte, 256) // 256-byte slice of 0's
for {
count, err := file.Read(data)
if err != nil {
return err
}
if count == 0 {
return nil
}
process(data[:count]) // if only 138 bytes were used, process those 138 only
}
B.6 Comments, godoc
Godoc go doc </path/to/package>[.identifier]
expects comment patterns directly
above package, type, constant, variable or function. For package, start with the
word “package” then the name of the package. For types and funcs, start with the
name. For individual consts and vars, start with the name. For const blocks,
just describe the block.
// Package cards provides card decks and cards for games. ❶
//
// The cards package can be used to represent game states with
// ranks and suits, e.g. English card games.
package cards
// These are the card game types defined in this package.
const (
STANDARD_DECK = iota // ❷
EUCHRE_DECK
BRIDGE_DECK
CANASTA_DECK
...
)
// Card represents an English card, with rank and suit. Face
// cards are also ranked with an integer as they also have value
// (traditionally, jack = 11 and so on).
type Card struct {
rank int
suit int
}
// CardDeck is a slice of Cards.
// It must be initiated with "make(CardDeck, int)" to be useful
// and then be populated and optionally shuffled by its Factory.
type CardDeck []card
// Populate initializes its CardDeck with the appropriate number
// and values of cards according to deckType.
// Use the constants defined above as arguments to populate the
// deck according to Hoyle, e.g. cards.STANDARD_DECK,
// cards.EUCHRE_DECK, etc.
func (cd *CardDeck) Populate(deckType int) bool {
cd[0] = Card{Joker, 0}
// Jump-table here
// ...
}
// silly demonstrates comments on a variable.
func silly() int {
// Gadzooks holds the number of gadzooks in a gadzookathon.
var gadzooks int = 10
return gadzooks
}
① This is for any public or private package that is not a command. A command, by
convention, starts with /*, the name of the program and a short description.
A long description follows, including Usage. It ends with */.
② iota is
explained below. In a nutshell, the compiler assigns integers to each constant
in the group sequentially.
For more, see https://tip.golang.org/doc/comment
B.7 Versioning
In a nutshell:
# Test the go files in all directories ...
go test ./... -cover -coverprofile=c.out
# ... passes!
SEMVER=<maj.min.patch-comment>
git add <whatever needs adding, or -u, or just plain .>
git commit -m "<Your team's chosen message format>"
git tag [-a|-s] v${SEMVER}
git push origin v${SEMVER}
For major version changes, read the Go docs.
C. Types
C.1 Literals
Literal | Examples |
---|---|
Integer | 1, 10_164, 0x7b, 0o644, 0b11110111 |
Floating Point | 3.14, 6.626e-34, a3p3b |
Rune | 'a', '\71', '\n', '\t' |
Interpreted String | "Hello!\n My name is George" |
Raw String | `Hello!\n does not print newline!` |
Complex Number (!!) | 23.35i5.3 |
C.2 Other Types
Unlike C, Go will initiate new primitives with a zero value unless specifically assigned a value by the developer.
Built-in Type | Zero Value | Comments |
---|---|---|
bool | false | Empty strings or 0 != false! |
int8, int16, int32, int64 | 0 | |
uint8, uint16, uint32, uint64 | 0 | |
byte (equiv to uint8) | 0 | |
int, uint (depends on CPU) | 0 | |
rune (equiv to int32) | 0 | |
float32, float64 | 0.0 | IEEE754: Avoid[^1] |
complex64, complex128 | (0.0,0.0) | (float32, float32) = complex64 |
string | "" | Immutable |
[]int, etc. | nil | Except []{}, which is 0 length |
[][]int, etc. | [nil] | Except []{}, which is 0 length |
map[type]type | nil | Do not create a nil map |
*whatever (pointer) | nil |
In the case of map[type]type, declare x := map[type]type{}
to create a
0-length map that can actually be used.
C.3 Functions with with int vs functions with uint
Note that any function that works with an integer type should have an equivalent for a unsigned integer type:
func ScrewUpThisInt(x int) int {}
func ScrewUpThisUint(x uint) int {}
D. Variables
D.1 Simple Declaration
All the following are valid.
Declaration | Comment |
---|---|
var x int = 10 | |
var x = 10 | Same. Default type of an integer literal is int |
var x int | Equivalent to var x = 0 |
var x, y int = 10, 20 | |
var x, y = 5, “Bob” | |
x := 10 | “Short Declaration Format” uses type inference |
x, y := 5, “Bob” |
D.2 Short Declarations
- ‘x := 10’ can assign a value to an existing variable. Causes shadowing in nested scopes.
- := is not legal outside functions (i.e. at the package level).
- := should not be used when initializing to the zero value
D.3 The Declaration List
var (
x int
y = 20
g = "George"
s, w = 13, "William"
)
D.4 Constants
From Jon Bodner directly: “Constants in Go are a way to give names to literals. There is no way in Go to declare that a variable is immutable.” Basically, consts are assigned only at compile-time.
const x int64 = 10 // Note x is lowercase. Do not CAPITALIZE
const g = "George" // outside package-level declarations.
const (
westmount = iota // Starts at 0
ville_marie // Automatically assigned 1 due to iota
southeast // Automatically 2
ndg // 3 etc...
cdn
)
Constants should be left untyped so they can be used flexibly.
D.5 Pointers
A pointer is a variable that holds a value’s memory address. * is the indirection operator. It returns the value at an address, called dereferencing. & is the address operator that returns the address of a variable’s value.
var x = 32
var pointerX *int // a memory address to an integer, set to nil
pointerX = &x // &x is, say, address 0xc00001c148 holding 32.
fmt.Println(x, &x, pointerX, *pointerX)
// 32 0xc00001c148 0xc0001c148 32
// * is the "dereference" operator that returns the value held
// at address pointerX
/* Rare */
var pointerY = new(int) // Creates a 0 and assigns its address
// to pointerY
Jon Bodner, in Learning Go: An Idiomatic Approach to Real-World Go Programming, posits that pointers are “a last resort.” The use of a pointer indicates that data is mutable; mutable may violate the code smell, “Mutable Data.”
E. Arrays, Slices, and Maps
E.1 Whither the Array?
Arrays are stricly limited in Go and may only be used in algorithms where the size of the array is understood at compile-time. Arrays, however, are the backbone of slices.
E.2 Array Declarations
var x [3]int // Assigns [0, 0, 0] to x
var x [3][4]int // Assigns
// [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
var x = [3]int{2, 4, 8} // Assigns [2, 4, 8]
var x = [...]int{2, 4, 8} // Also assigns [2, 4, 8] to x
var x = [6]int{1, 3:2} // Assigns [1, 0, 0, 2, 0, 0] to x.
// Remember: Arrays are 0-indexed!
Given a declaration var x = [3]int, understand that [3] is considered part of the type declaration. Know the following errors:
a := 2
var x [3]int
var y [4]int
var z [3]float64
var b [a]int // Bang! Error. ❶
if x == y { // Bang! type [3]int != type [4]int
// ...
}
c := int(z) // Bang! Type conversion does not work on arrays.
➀ Array types are resolved at compile time, so variables cannot be permitted in the type declaration.
E.3 Slice Declarations
Slices have variable lengths. The zero value is nil.
var x []int // x = nil
var x [][]int // x = [nil]
var x = []int{} // x = non-nil zero-length. ❶
var x = []int{1, 2, 3, 5, 7, 11} // ❷
var x = []int{1, 3:2, 4} // Assigns [1, 0, 0, 2, 4] to x.
➀ One does not want non-nil zero-length unless converting a slice to JSON.
➁
Compare to [...]int
used in Arrays.
You can do the following with slices:
var x = []int{3, 5, 2}
fmt.Println(len(x))
y := append(x, 13) // y has been assigned to [3, 5, 2, 13]
y = append(x, 13, 17, 19) // y is now [3, 5, 2, 13, 17, 19]
b := y[3:5] // Subslice [13, 17], i.e. [3] to just before [5]
E.3.1 Capacity
A slice is a struct with
- a pointer to the start of an Array
- a length variable counting the number of elements used
- a capacity variable reporting the length of the Array.
The Array may be longer than reported by the length variable. If one uses append() on a slice with a full Array, then append() creates a new internal Array with double the original size, reassigns the pointer to point to it, updates the length counter by just one, and garbage collects the old Array.
x:= []int{1, 2, 3, 5, 7, 11}
fmt.Println("Length of x:", len(x)) // 6
fmt.Println("Capacity of x:", cap(x)) // 6
x = append(x, 13)
fmt.Println("Length of x:", len(x)) // 7
fmt.Println("Capacity of x:", cap(x)) // 12!
E.3.2 Subslices
Alert! Remember that slices contain a pointer to an Array. When creating a subslice, the length and capacity variables are copied, but the Array pointer is shared. What this means is: when taking a subslice from a slice, changes to the Array will be reflected in both the derived slice and the original … but only up to a point. Appending past the capacity of the original slice will not be reflected. The capacity and length counters will be updated in the copy, but not the original:
x := []int{1, 2, 3, 5, 7, 11}
y := x[2:5] // i.e. {3, 5, 7}
y[0] = 13
fmt.Println(y) // {13, 5, 7} OK
fmt.Println(x) // {1, 2, 13, 5, 7, 11} Whut?
y = append(y, 17)
fmt.Println(y) // {13, 5, 7, 17} OK
fmt.Println(x) // {1, 2, 13, 5, 7, 17} NOK!
y = append(y, 19)
fmt.Println(y) // {13, 5, 7, 17, 19} OK
fmt.Println(x) // {1, 2, 13, 5, 7, 17} NOK: 19 never made it
The mutability would make a Rust programmer weep. The answer is to do a full copy:
x := []int{1, 2, 3, 5, 7, 11}
y := make([]int, 3)
copy(y, x[2:5]) // Copies [1, 2] into y and returns '2'.
y[0] = 13
fmt.Println(y) // {13, 5, 7} OK
fmt.Println(x) // {1, 2, 3, 5, 7, 11} OK
E.3.3 The Curious Case of Passing Slices to Functions
As evidenced in E.3.2, a slice consists of a pointer and two integer variables. Go is a value-copy language, meaning that when arguments are passed into a function, those arguments are copied into new memory. Just as in the case of modifying subslices, though, there is a pointer nestled among the copied values.
A SLICE IS AN INVITATION TO SIDE EFFECTS. Either COPY the slice inside the function, or pass a copy to the function.
// doHorribleThings does nothing of the sort. The w_ prefix in
// this example means "working (local) copy"
func doHorribleThings(x []int) []int{
w_x := make([]int, len(x))
copy(w_x, x)
// Do things to i_x
return w_x
}
x := []int{1, 2, 3, 5, 7, 11}
y := doHorribleThings(x)
E.3.4 No-no’s
You cannot do the following with slices:
func main() {
var x = []int{1, 2, 3}
var y = []int{1, 2, 3}
if x == y { // Bang!
// ...
}
if reflect.DeepEqual(x, y) { // Much better
// ...
}
}
However, if x == nil is permissible.
E.3.4 Slices and Strings
String literals are encoded UTF-8, which means 1 to 4 bytes per character. Use string slices only when using 1-byte character sets. Otherwise, use the strings or unicode/utf8 packages.
E.4. Maps
In a nutshell:
foo := map[string]string {
"rank": "Detective",
"sn": "Edwards",
"firstname": "Jay"
}
foo["rank"] = "Agent"
foo["identity"] = 'J'
delete(foo, "sn")
delete(foo, "firstname")
bar := make(map[int][]string, 10) // One integer-keyed map of
// ten string arrays
Since Maps use pointers, that makes them mutable. As such, they should not be used as input parameters or return values unless copied … or use structs instead (far below).
E.5 Maps as Sets
Go does not include sets as a primitive. Maps are used instead to simulate them.
// Anything not true is false as false is the zero value
values := []int{2, 18, 52, 18}
mySet := make(map[int]bool)
for _, v := range values {
mySet[v] = true
}
fmt.Println(mySet) // [2:true 18:true 52:true]
if mySet[5] {
fmt.Println("This shouldn't happen.")
} else {
fmt.Println("5 is not in the set.")
}
if mySet[2] {
fmt.Println("2 is in the set.")
} else {
fmt.Println("This shouldn't happen.")
}
Maps cannot mix types. For that, one needs structs.
F. Control Structures
F.1 Blocks
- The universe block contains all the predefined identifiers. They can be shadowed … beware!
- The package block
- The file blocks
- The function blocks.
- The control blocks.
// Package block
import (
// file block
"fmt"
"time"
)
func myFunction() /* block */ {
// function block
}
F.2 Control Blocks
F.2.1 If
x = 0
if x > 5 { // control block
...
} else if { // control block
} else { // control block
}
Variables can be declared in the “condition scope”:
if x := myFunction(); x > 5 {
...
} else {
}
fmt.Println(x) // Bang! Compile error
F.2.2 For
// Complete for
for i := 0; i < 10; i++ {
}
// Condition-only for
for i < 10 {
...
i = i + 1
}
// Infinite for
for {
if CONDITION_X {
continue
}
...
if CONDITION_Y {
break
}
}
// For-range loop
values := []int{1, 2, 3, 5, 8, 13}
for i, v := range values {
// You must actually *do* something with i (the index)
// and v (the value at i) or get a compiler error
}
// Range works with maps as well, but idiomatically use k, v
// instead of i, v.
for _, v := range values {
// Do something with v (the value at the index).
// "_" does not need references
}
Notes
- No whiles, dos, or do whiles. Just the above.
- In the case of maps, iterating will have random results.
- fmt.Println(map) will always print in ascending order of key.
- The special case of strings: for-range will go through the runes and convert any UTF-8 special characters into an int32.
F.2.3 Labels
break and continue can break through several blocks with the use of labels.
func labelDemo() {
x := [][]int{{1, 3, 5, 7}, {2, 4, 6, 8}, {12, 14, 16, 18}}
outer:
for _, v := range x {
fmt.Println(v)
inner:
for _, vv := range v {
if vv == 5 {
continue outer // Goes to next v
} else if vv == 4 {
continue // Goes to next vv
}
fmt.Println(vv)
}
}
Of course, this example is contrived: “continue outer” is a weird break.
F.3 Switch
Switches in Go do not fall through like in C. Use multiple matches. Note that case is started on the same column as switch.
const animal_field = 1
line := []string{"20230831","robin","lake louise"}
switch animal = line[animal_field]; animal {
case "cow":
mammals += 1
case "bluejay", "robin":
birds += 1
case default:
unknown += 1
}
F.3.1 Blank Switch
A regular switch checks for equality. A blank switch can use any comparison.
switch {
case totalAmount <= 5000.0:
... // theft under 5000
case totalAmount > 5.0 && totalAmount <= 100000.0:
... // theft under 5000 but who cares?
default:
... // Send the detective!
}
Switches are basically if/then. Use it idiomatically when the cases are related.
G. Structs, Functions, and Object-Oriented Programming
G.1 Structs
Structs are like classes but contain only attributes.
type agent struct {
badge string
sn string
firstName string
precinct precinct
}
// The developer *must* set *every* value when using :=
j := agent{"12J43", "Edwards", "James", westmount}
var k agent
k = agent{
badge: "02K34",
sn: "Brown",
firstname: "Kevin"
// here, precinct is set to the zero value
}
fmt.Println(j.badge)
// Pointers!
pz := &agent{"00Z24", "Scott", "Zachary", ville_marie}
// Notice there is no requirement for * here, but ...
fmt.Println("Zed", pz.badge)
// .. if you really want to dereference, use parentheses
fmt.Println("Zed", (*pz).badge
Structs can only be compared with the same type of struct. However, type conversions exist if the fields have the same name, order and types.
G.1.1 Structs with pointer fields
One cannot assign literals to a struct pointer field. Construct a helper function. (Bear in mind a struct is not a literal so the indirect and address operators work just fine.)
type agent struct {
badge string
sn string
firstName string
precinct *string
}
func stringPtr(s string) *string {
return &s
}
func main() {
/* This will fail
j := agent{"12J43", "Edwards", "James", "Westmount"}
*/
j := agent{"12J43", "Edwards", "James", stringPtr("Westmount")}
}
G.1.2 Anonymous Structs
computerLanguage := struct {
name string
inventors []string
}{
name: "C",
inventors: []string["Ritchie"]
}
G.2 Functions
- There are no named and optional input parameters. If needed, use a struct.
- Functions copy values for local use (“call by value”), so they don’t modify the data from the calling function. This is good! It avoids one of the call smells, “Mutable Data.” If one needs to modify the data by calling a function, use pointers (or better reassign a variable from a function return).
func functionName() {
...
}
func functionWithIntegerReturnValue() int {
...
return integerReturnValue
}
func functionWithParameter(argument int) int {
...
return integerReturnValue
}
func functionWithMultipleParamsAndReturns(arg1 int, arg2 int) (int, bool) {
// Multiple return values are automatically enclosed
// in parentheses.
// ...
return returnValue, true
}
a, ok = functionWithMultipleParamsAndReturns(2, 4) // OK
a = functionWithMultipleParamsAndReturns(2, 4) // Bang!
_, ok = functionWithMultipleParamsAndReturns(2, 4) // OK
func functionWithSimilarParameters(arg1, arg2 int) (int, error) {
// Same as above, but using the error pattern.
// error is always last in multiple return values.
// ...
return returnValue, nil
}
func namedReturns(arg1, arg2 int) (namedResult1 int, namedResult2 error) {
return namedResult1, namedResult2
}
func blankReturns(arg1, arg2 int) (namedResult1 int, namedResult2 error) {
// Blank returns work, but are considered an antipattern
namedResult = 5
namedResult = nil
// Do other stuff.
return // returns 5, nil
}
// anonymousFunction prints "Hello, Syd"
func anonymousFunction() {
func(addressee string) {
fmt.Println("Hello,", addressee)
}("Syd")
}
// closure demonstrates that functions are first-class
// objects
func closure() (func(string), error) {
/* A function inside a function using the outside
* function's variables or arguments is called a
* Closure.
*/
greeting := "Hello,"
sayHello := func (addressee string) {
fmt.Println(greeting, addressee)
}
return sayHello, nil
}
func main() {
myfunc, err := closure()
if err != nil {
return
}
myfunc("Anca") // prints "Hello, Anca"
}
func variadicArguments(values ...string) {
/* called with
* variadicArguments(a...) or
* variadicArguments("foo", "choo") or
* variadicArguments([]string{"foo", "choo"}...).
* Note the ... for literals AND variables.
*/
fmt.Println(len(values))
for _, v += range values {
...
}
...
}
// Variadic parameters MUST be the _last_ or _only_ arguments.
func brokenVariadicArguments(values ...string, lastArgument string) {
...
} // Bang!
// thisHasADeferBlock demonstrates the defer statement
func thisHasADeferBlock() namedError error {
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
// Anonymous function to be executed on return
defer func() {
error = setUpAnError(5)
}() // Note the parenthesis!
// No matter what happens, the file is closed.
defer f.Close()
// Defers are run in last-in,first-out order.
defer fmt.Println("Me first")
// ...
return nil // The defers are run _after_ return.
// "nil" here will change because the
// anonymous defer function changes the
// namedError
}
In the case of the defer block, if it has to modify return values, use named return values.
G.2.3 Mutable parameters
Pointers mean the parameters are mutable. Mutable data is a code smell, but in the case of interfaces and large structs, pointers are advised. Just be careful.
Large structs means > 1 MB.
/* Read the caveat about mutable data */
func mutableParameter(a *agent) {
a.firstName = &localName
}
mutableParameter(&a)
fmt.Println(a.firstName) // Prints Bob. If we weren't using pointers, the
// original name would remain since Go is value copy.
G.2.3 Function Signatures and Jump Tables
Functions are values in Go, and can be passed as arguments. Function signatures are considered types: in this case functionName(person) error
type person struct {name string}
func crush(victim person) error {return nil}
func stretch(candidate person) error {return nil}
func main() {
jumpTable := map[string]func(person) error{
"crush": crush,
"stretch": stretch,
}
johnny := person{"Johnny"}
fmt.Println(jumpTable["crush"](johnny))
}
NOTE: Jon Bodner would have a cow with the last line since there is no error checking: “Error handling is what separates the professionals from the amateurs.”
Function types can be declared for OOP (interface bridges) and documentation. Here is a subtle rewrite of the above:
type person struct {name string}
type tortureFunc func(person) error
func crush(victim person) error {return nil}
func stretch(candidate person) error {return nil}
func main() {
jumpTable := map[string]tortureFunc{
"crush": crush,
"stretch": stretch,
}
johnny := person{"Johnny"}
fmt.Println(jumpTable["crush"](johnny))
}
G.3 Methods
Methods are assigned to types. This acts effectively to make a class. The declaration is func (r receiver) funcName(args…) (returnValues) {}. The receiver (which is a type) is what makes a function into a method. Never use “this” or “self” as the receiver identifier.
type card struct {
rank int
suit rune
}
/* I am aware of the unicode full deck of cards. No, I'm not using them.
* Toy problem!
*/
func (c card) reveal() string {
switch {
case c.rank == 0:
return fmt.Sprintf("JK")
case c.rank == 1:
return fmt.Sprintf("%cA", c.suit)
case rank <= 10:
return fmt.Sprintf("%c%d", c.suit, c.rank)
}
return fmt.Sprintf("%c%c", c.suit, faces[c.rank-11])
}
Methods are not restricted to structs.
type superInt int
func (s *superInt) plusequal(addend int) {
*s = *s + superInt(addend)
}
var myInteger superInt = 5
superInt.plusplus(6)
fmt.Println(superInt)
G.3.1 Pointer and Value Receivers
If a method for a type must change the receiver or deal with a nil receiver, it must use receiver pointers: func (r *receiver) …. A common convention is to use pointers for all type methods if any of them modify the receiver.
Also: write code to handle a nil value if you don’t want to use panics.
G.3.2 Method Sets
Any series of methods (value or pointer receiver) assigned to a type make a method set. These are used in interfaces.
G.3.3 Embedding
Embedding is not inheritance but sure looks like it. When embeddeding a struct in another struct as a field, its methods and fields are promoted to the containing struct.
type Position struct {
x int
y int
z int
}
type Fish struct {
Position // Embedded non-named struct
tailPosition int
oxygenLevel int
tastiness int
}
tuna := Fish{
Position: Position{23, -23, 5}, // note "Position: Position"
tailPosition: 0,
oxygenLevel: 23,
tastiness: 100,
}
tuna.x++ // x is part of the tuna.
G.4 Interfaces
To declare an interface, create an interface type and populate it with its required methods.
type LifeFunctioner interface {
move(int, int, int)
respire()
eat(string)
drink(string)
}
Interfaces are implicit. One does not need to declare that a type implements an interface: if the type receives all the required methods it automagically implements the interface.
func (f *Fish) move(x, y, z int) {...}
func (f *Fish) respire() {...}
func (f *Fish) eat(food) {...}
func (f *Fish) drink(beverage) {...}
The Fish type now implements the LifeFunctioner interface!
type FoodChainMember struct {
eats LifeFunctioner
eatenBy LifeFunctioner
}
pescatarian := FoodChainMember{
eats: Fish{...},
eatenBy: nil,
}
- Passing interfaces as parameters means using the heap for each interface parameter instead of the stack.
- Developer wisom: accept interfaces, return structs. This goes for factory functions as well.
- Use reflection to determine if an interface is nil.
H. More on Errors
H.1 Wrapping an Error
func fileChecker(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("in fileChecker: %w", err)
}
}
Use defer() if the wrapping a whole bunch of errors with the same message. Notice the use of the named return.
func throwsAnError() (_ string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("in throwsAnError: %w", err)
}
}() // Remember the parenthesis for anonymous functions?
val1, err = doSomething1()
if err != nil {
return "", err
}
val2, err = doSomething2()
if err != nil {
return "", err
}
}
H.2 Creating your own Error
type ErrTooEvil struct {
/* The struct can be anything you want. In fact, the type does not even
* have to be a struct. */
status int
message string
moronWhoCausedThisMessage userID
}
/* The error{} interface has only one method: Error(). */
func (e ErrTooEvil) Error() string {
return e.message + string(moronWhoCausedThisMessage)
}
H.3 Sentinel Error
A Sentinel Error is an error that indicates processing cannot start or continue. It is created at the package level, and creating one makes it part of your public API.
H.4 Is and As
errors.Is(err, Error) checks for a specific instance or equavalent values in a wrapped chain of errors.
errors.As(err, &Error) checks for an error type in a wrapped chain of errors.
var ErrBadThingA = errors.New("bad thing a")
type BadThingErr struct {
signal int
message string
}
func (ebtc BadThingErr) Error() string {
return fmt.Sprintf("ErrBadThingC: %s", ebtc.message)
}
func wrapsErrors() error {
if <terrible thing a> {
return fmt.Errorf("wrapsErrors: %w", ErrBadThingA)
}
if <terrible thing b> {
return fmt.Errorf("wrapsErrors: %w", BadThingCError{5, "bad thing b"})
}
return nil
}
func main() {
err = wrapsErrors()
if errors.Is(err, ErrBadThingA) {
// process the error
} else if errors.As(err, &BadThingErr) {
// process the error
}
}
I. The net/http Standard Library
As Go is excellent for Web applications (I would rather use Rust for games), net/http is a package of interest.
I.1 net/http Client
Used to send http requests and process responses. Think of Python’s request library or Ansible’s url resource. It uses contexts, which is a metadata construct for things like user ID’s, timeouts, browser versions etc, that need to be maintained through request chains.
targetUrl := "https://cummings-online.ca/coding/go_environment"
client := &http.Client{ // One client for all requests. It runs concurrently.
Timeout: 30 * time.Second,
}
req, err := http.NewRequestWithContext(
context.Background(), http.MethodGet, targetUrl, nil,
)
req.Header.Add("User-Agent", "ExampleGobot/0.1 https://example.com/bot.html")
response, err := client.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
panic(res.Status)
}
fmt.Println(res.Header.Get("Content-Type"))
fmt.Println(res.Body)
I.2 net/http Client with REST API responses
If reading a REST API response, use json and a template to pull the data. Note the use of struct tags.
var restResponseTemplate {
MonitorStatus int `json:"status"`
SystemNames []string `json:"system_names`
LastReboot time.Time `json:"last_reboot"`
}
err = json.NewDecoder(res.Body).Decode(&data) // "Unmarshalling" JSON
fmt.Println(data)
For reference, “context.Background()” creates an empty context.
I.3 net/http Server
Before starting the Go standard library http server, one must set up a handler to send to the server instance.
I.3.1 Simple HTTP Handler
Any type that has a ServeHTTP(http.ResponseWriter, *http.Request) function becomes an HTTP handler. One must do the following in order:
- Create a Header (optional: use only if creating response headers)
- Write the Header (optional: http.StatusOK (200) does not require it)
- Write the Body.
This is a single page handler, which is not too useful since all it does is throw 404s:
type MyHandler struct{}
func (mh MyHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
header := rw.Header()
header.Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusNotFound)
rw.Write([]byte("<html><body><h1>404. Hahaha.</h1></body></html>"))
}
I.3.2 Multiplex HTTP Handler
myHandler := http.NewServeMux()
myHandler.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request){
header := rw.Header()
header.Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusNotFound)
rw.Write([]byte("<html><body><h1>404. Hahaha.</h1></body></html>"))
})
myHandler.HandleFunc("/george", func(rw http.ResponseWriter, r *http.Request){
header := rw.Header()
header.Add("Content-Type", "text/html; charset=utf-8")
rw.Write([]byte("<html><body><p>This is George.</p></body></html>"))
})
I.3.3 Invoking the HTTP Server
server = http.Server{
Addr: fmt.Sprintf(":%d", port),
ReadTimeout: readTimeout * time.Second,
WriteTimeout: writeTimeout * time.Second,
IdleTimeout: idleTimeout * time.Second,
Handler: MyHandler,
}
err := server.ListenAndServe()
if err != nil {
if err != http.ErrServerClosed {
panic(err)
}
}
I.3.4 Parent Mux, Child Mux
RESTful API’s have a series of paths. The pattern is to create a mux for each level of the path, strip the level and pass the remaining path to children muxes.
By the way, this is NOT how to handle 404 errors idiomatically, but it serves as a learning mechanism.
type HTTP404Handler struct{}
func (h4h HTTP404Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
header = rw.Header()
header.Add("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusNotFound)
rw.Write([]byte("<html><body><p>404. Hahaha.</p></body></html>"))
}
employeeHandler := http.NewServeMux()
resourceHandler := http.NewServeMux()
rootHandler := http.NewServeMux()
// ... etc ...
rootHandler.Handle("/", HTTP404Handler{}) // Note the {} since it's a struct
rootHandler.Handle("/employee/", http.StripPrefix("/employee"), employeeHandler)
rootHandler.Handle("/resource/", http.StripPrefix("/resource"), resourceHandler)
I.3.5 Middleware
Middleware is a pattern that applies a common series of actions across multiple handlers. Basically, it takes a http.Handler instance and returns another one.
The following code is silly, since all one needs to do to log a 404 is throw log.Info() into HTTP404Handler’s ServerHTTP() method but we’ll just middleware the sucker anyway.
import (
log "github.com/sirupsen/logrus"
)
func Log404Access(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request){
log.Info(fmt.Sprintf("%s [%d]", r.URL.Path, http.StatusNotFound))
h.ServeHTTP(rw, r)
})
}
rootHandler.Handle("/", Log404Access(HTTP404Handler{}))
BTW, Jon Bodner doesn’t like the standard http.ServeMux library for middleware. He encourages folks to try gorilla mux or chi.
J. Concurrency
Blocking on IO reads from services and files is not fun. Enter concurrency.
- Go concurrency uses go routines. They are usually closures (anonymous functions inside a function).
- Since go functions cannot return values, they make use of channels.
- A full channel blocks until it is read.
- However, a channel that is closed can be read, returning the zero value.
- Since a closed channel is always readable, use the Comma OK idiom.
- A closed channel must be closed once, and once only.
- Since multiple channels can cause deadlocks, channels that are able to be read or written to are selected randomly in a select block.
- Usually, concurrency does not belong in any public API.
In a nutshell:
ch1 := make(chan string) // Unbuffered channel. Most channels are like this.
// Unbuffered channels are blocked until they have been
// read.
ch2 := make(chan int, 2) // Buffered channel with two places.
func main() {
go func {
y := "Concurrency isn't all that difficult."
ch1 <- y
}() // Remember anonymous functions end with ()!
go func {
x := 5
y := 7
ch2 <- x
ch2 <- y
}()
select {
case result, ok := <- ch1:
if !ok {
fmt.Println("Closed channel 1")
ch1 = nil // This case will never need to be checked again
// (not necessary here, but sure needed for a loop)
// continue // Only needed if in a loop.
}
fmt.Println(result)
case result, ok := <- ch2:
if !ok {
fmt.Println("Closed channel 2")
ch2 = nil
}
fmt.Println(result)
}
close(ch1)
close(ch2)
}
In the above case end, the output would be either a string or one int. One would not know which one beforehand as select{} is random.
J.1 Done Channel Pattern
When using a “for…select” loop, there is a danger of goroutine leaks when the loop breaks. Use the Done Channel Pattern.
Note the shadowing in the first go func and the value passing in the second. This prevents the last value of i only being copied to each iteration of the go routines.
// This is a terrible, terrible useless 2-pass loop just to demonstrate the
// Done Channel Pattern. It fires up four concurrent, blocking goroutines.
func uselessTwoPassLoop() {
done := make(chan struct{}) // Empty struct has size 0
i = 0
for {
i++
go func {
i := i // Shadowing is necessary when using parent variables that
// change in a loop.
fmt.Println(i)
select {
case <- done: // Blocked until "done" is closed.
}
}()
go func(i int) {
fmt.Println(i)
select {
case <- done: // Blocked until "done" is closed.
}
}(i)
if i > 2 {
break
}
}
close(done) // Stops the go funcs
}
J.2 Cancel Function
We take the above example and return a cancel function to ensure the calling routine cleans up the goroutines. This pattern shows up in contexts (below).
func uselessLoops(max int) func() { // func() is a return value
done := make(chan struct{}) // Empty struct has size 0
cancel := func() {
close(done)
}
for i := 0; i < max; i++ {
go func {
i := i
fmt.Println(i)
select {
case <- done:
}
}()
go func(i int) {
fmt.Println(i)
select {
case <- done:
}
}(i)
}
return cancel
}
func main() {
cancel = uselessLoops(5)
cancel()
}
J.4 Waitgroups
Waitgroups are an alternative method to done patterns on multiple goroutines.
var wg sync.WaitGroup
wg.add(2)
go func() {
defer wg.Done()
// code here
}
go func() {
defer wg.Done()
// code here
}
wg.Wait()
K. Contexts
K.1 Basic Contexts
Contexts are instances that meet the context interface. They are used to hold metadata over requests. For example, http requests might have a user_id. Other IO may use contexts to signal timeouts and cancellations (e.g., if one service fails, there may be no point in reading from the others).
Contexts are passed as the first parameter to a function with the local variable ctx as a convention.
func doNothingWithThis(ctx context.Context, x int) (int, error) {
return x, nil
}
To create an empty context:
ctx:= context.Background()
K.2 Adding to Contexts in an Http Handler
Don’t bother trying this until you understand interfaces (way above), and handlers, mux.HandleFunc, and Middleware detailed in net/http.
-
An http request will return its context when asked.
func MiddleWare(handler http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){ ctx := context.Context() }) }
-
If additional metadata needs to be added to the context, wrap a key/value pair with a context factory function.
type uidKey int // never export. Used to prevent collisions between packages const key uidKey = 1 func MiddleWare(handler http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){ ctx := context.Context() // sign and place in a cookie or a custom header. ctx = context.WithContext(key, "bcef01a98d63" }) }
-
Create a new http request with the updated context before sending the context along to a handler (or another service).
type uidKey int const key uidKey = 1 // never export func MiddleWare(handler http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){ ctx := req.Context ctx = context.WithContext(key, "bcef01a98d63") req = req.WithContext(ctx) handler.ServeHTTP(rw, rq) }) }```
Some useful contexts are:
- ctx, cancel := context.WithCancel(ctx). “cancel” is a convention. Use cancel() at least once. Works on http.NewRequestWithContext(), among others.
- ctx := context.WithTimeout(ctx, time.Duration). Autocancels after duration
- ctx := context.WithDeadline(ctx, time.Time). Autocancels at set time.
L. Unit Testing
Tests are written in the same directory as the files they are testing, with “_test.go” appended to the filename. This one-pager shows just the basics. Testing can additionally involve:
- testdata directories
- helpful third-party modules
- table tests
- benchmarking
- stubs
All in another blog post.
// File: odd_things.go
func addNiToString(x string) string {
return fmt.Sprintf("%sni", x)
}
// File: odd_things_test.go
// TestMain is an optional set up function
func TestMain(m *testing.M) {
var foo string = "bar"
}
// helperFunction is an optional function for DRY
func helperFunction(t *testing.T) {
os.doStuff(...)
// optional Cleanup cleans temporary resources
t.Cleanup(func() {
// remove resources
})
}
func Test_addNiToString(t *testing.T) {
result := addNiToString("Bob")
if result != "Bobni" {
t.Error("incorrect result: expected Bobni, got", result )
}
}
[^1]: According to
Maxim Gradan,
floating point is meant for computer graphics, where “good enough” is well, good
enough and fast. If one needs accuracy, use Gonum.
go install gonum.org/v1/gonum@latest