The Go Programming Lang
- GOPATH
- Run, build and install
- Dependencies
- Formatting
- Documentation
- Structuring a source tree
- The lang
- Basic Data Types
- Functions
- Error Handling
- Packages and Libraries
- Advanced Data Types
- Cool cool cool
- Libraries
Effective Go is a howto on writing idiomatic Go.
Toolchain⌗
GOPATH⌗
It is convention for all Go code to be hosted in a single workspace, and GOPATH
points to the root of this workspace.
A Go workspace contains one or more project repositories. Each repository is independently version controlled.
Inject this into ~/.profile
and include the $GOPATH/bin
on PATH
:
export GOPATH=$HOME/go
export PATH="$PATH:$GOPATH/bin"
Using go env
validate GOPATH
:
cd `go env GOPATH`
Run, build and install⌗
Source is placed within the src
dir within $GOPATH
like $GOPATH/src/github.com/bm4cs/cool-app/main.go
To just run a program, without building a binary:
$ go run main.go
hello world
To build a statically linked binary:
$ go build
$ file demo
demo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked
Cross compiling is available too:
$ GOOS=windows go build
$ file demo.exe
demo.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
The binary can be written to $GOPATH/bin
(or $GOPATH/pkg
for libraries) which is normally on the PATH
:
$ go install
$ ls $GOPATH/bin | grep demo
demo
Dependencies⌗
TODO: modules
$ go get github.com/golang/example/hello
$ $GOPATH/bin/hello
Hello, Go examples!
Formatting⌗
Format a file to stdout
:
$ gofmt main.go
Diff:
$ gofmt -d main.go
Update (write) the original source file:
$ gofmt -w main.go
Documentation⌗
Local CLI help:
$ go doc fmt Printf
func Printf(format string, a ...interface{}) (n int, err error)
Printf formats according to a format specifier and writes to standard
output. It returns the number of bytes written and any write error
encountered.
Local documentation HTTP server:
$ godoc -http :7070
Structuring a source tree⌗
project-layout curates common conventions followed by the community.
Check out go-structure-examples for flat, layered, modular and DDD based layouts.
The lang⌗
Variables⌗
There’s no such thing as a undefined variable in Go. Each data type has a well defined zero value.
var a int //set to 0
var b int = 10
var d, e, f bool
var (
g int
h string
i int = 1234
j, k, l bool
)
Inside a func can use short variable declaration (a declaration and assignment in one step):
m := 1
n, o := 2, 3
Assignments:
e, f = f, e
a := 11
a, p := 100, 200
The blank identifer (for ignoring outputs):
a, _ := f()
g(a)
Control structures (if, switch and for)⌗
if⌗
Go conditions uniquely supports an initialisation statement:
if err := f(x); err != nil {
return err
}
Ternary operators are not supported, due to poor readability.
switch⌗
Case statements do not fallthrough in Go.
Supports vanilla style:
var model string
//...
switch model {
case "dell":
// Do something
case "acer":
// Do something else
case "compaq":
// Do something different
default:
// Do nothing
}
However, unlike many langs Go supports another switch-expressionless form:
var velocity int
//...
switch {
case velocity == 0 || velocity == 1:
//...
case velocity >= 10:
//...
case f(velocity) >= f(100):
//...
}
If fallthrough is required there are a couple of options:
case velocity >= 10:
//...
fallthrough
Or pile up the case statements with multiple expressions:
switch motion {
case "walk", "run":
for⌗
The for
keyword covers off all loop types in Go. No support for while
, do
, until
loops exist.
- For loop
for i=0; i<10; i++
- While loop
for i<10
- Infinite loop
for
- Enumerable iteration
for i, v := range s
I/O⌗
fmt⌗
The fmt
(fumpt) package does formatted I/O.
- parameters are separated by spaces by default
- it can handle a variety of input types
- explicit argument indexes (one-based) are supported
fmt.Sprintf("%[2]d %[1]d\n", 11, 22)
Supported format verbs are like C, but simpler.
%v
value in default format%#v
print the value in valid Go syntax, handy for dumping structs or slices%t
boolean%d
base 10%b
base 2%x
base 16%s
raw string%T
type of the value
r, g, b := 124, 87, 3
// ...as #7c5703 (specifying hex format, fixed width, and leading zeroes)
fmt.Printf("#%02x%02x%02x\n", r, g, b)
// ...as rgb(124, 87, 3)
fmt.Printf("rgb(%d, %d, %d)\n", r, g, b)
// ...as rgb(124, 087, 003) (specifying fixed width and leading zeroes)
fmt.Printf("rgb(%03d, %03d, %03d)\n", r, g, b)
// ...as rgb(48%, 34%, 1%) (specifying a literal percent sign)
fmt.Printf("rgb(%d%%, %d%%, %d%%)\n", r*100/255, g*100/255, b*100/255)
// Print the type of r.
fmt.Printf("%T\n", r)
Reading input done with fmt.Scan
, fmt.Scanln
and fmt.Scanf
var s string
var n int
cnt, err := fmt.Scan(&s, &n)
fmt.Println(cnt, s, n, err)
Scanf
will parse input precisely as the format string specifies (in the snippet below: an ‘a’ character, followed by a space, a string, a newline, then an integer):
fmt.Scanf("a %s\n%d", &label, &age)
Fuller example:
var n1, n2, n3, n4 int
var f1 float64
// Scan the card number.
str1 := "Card number: 1234 5678 0123 4567"
_, err := fmt.Sscanf(str1, "Card number: %d %d %d %d", &n1, &n2, &n3, &n4)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%04d %04d %04d %04d\n", n1, n2, n3, n4)
// Scan the numeric values into a floating-point variable, and an integer.
str2 := "Brightness is 50.5% (hex #7ffff)"
_, err = fmt.Sscanf(str2, "Brightness is %f%% (hex #%x)", &f1, &n1)
if err != nil {
fmt.Println(err)
}
fmt.Println(f1, n1)
CLI⌗
Args⌗
- complete command line is available via an array named
os.Args
- use
len()
to sizeos.Args
- iterate over individual args with
range
likefor _, e := range os.Args
Flags⌗
The flag
package is a standard package for parsing CLI flags ./coolprog -verbose -count=7
It works by creating a pointer for each flag. Flags are then evaluated with flag.Parse()
.
verbose := flag.Bool("verbose", false, "Print verbose log outputs")
count := flag.Int("count", 1, "Number of items to collect")
flag.Parse()
fmt.Printf("verbose = %t, count = %d\n", *verbose, *count)
When working with the flag pointers, they need to be dereferenced with the *
syntax.
By declaring variables explicitly can avoid the need to deref pointers:
var max int
flag.IntVar(&max, "max", 1, "The maximum number of shares to buy on a triggered event")
Unfortunatly flag
does not support POSIX style flags (i.e both short and long forms -n --name
). Community packages such as pflag
support this.
Basic Data Types⌗
Type conversion⌗
Many langs support type casting. Go provides conversion functions.
var u uint = 64
var u64 unit64 = uint64(u)
When truncation is needed, go always rounds down towards zero.
Strings⌗
- the
strings
package is where many string utility functions live for working with bytes (strings.IndexByte
) and runes (strings.IndexRune
) - the
strconv
package provides many conversions (such asAtoi
,Itoa
,FormatInt
,ParseBool
,FormatFloat
,ParseFloat
) - the unicode package offers many identification and conversion utilities such as
IsSpace
orToTitle
exist. - they are immutable (mutability is possible using a slice of bytes)
- treated as sequences of bytes with arbitrary values, even a null byte, differentiating them from C strings
- substrings
s[1:4]
would return charactershis
in the below string (the second index hits the first byte after the substring) - substring shortcuts
s[:4]
ands[4:]
Sample string:
| T | h | i | s | |
^ ^ ^ ^ ^
0 1 2 3 4
Unicode⌗
Go has first class UTF-8 support. UTF-8 is a brilliant variable length encoding invented by Ken Thompson of UNIX and C fame.
Go uses UTF-8 to encode unicode. Recall UTF-8 is a variable length encoding (i.e. it can use 1-byte or upto 4-bytes).
fmt.Println(len("a")) // 1
fmt.Println(len("ä")) // 2
fmt.Println(len("走")) // 3
We can see the UTF-8 encoding in action here, with simple English characters using only a single byte, with the more esoteric characters using 2 or more bytes depending how deep into the unicode listing they fall.
Go allows you to treat strings at both the byte and character level.
Runes⌗
At the character level, the unit of a character is called a rune
. A rune
is just an alias for a int32
.
Runes are presented by range
when iterating over a string:
for i, e := range "abä走." {
fmt.Println("range:", i, e, string(e))
}
// range: 0 97 a
// range: 1 98 b
// range: 2 228 ä
// range: 4 36208 走
// range: 7 46 .
Strings can be worked with at both the byte and rune levels. UTF-8 generally makes this work out fine:
fmt.Println("IndexByte:", strings.IndexByte("abä走.", '.')) // IndexByte: 7
fmt.Println("IndexRune:", strings.IndexRune("abä走.", '走')) // IndexRune: 4
The unicode/utf8 package provides rune aware functionality, such as RuneCountInString.
str := "Hello, 世界"
fmt.Println("bytes =", len(str)) // bytes = 13
fmt.Println("runes =", utf8.RuneCountInString(str)) // runes = 9
Showcase of some unicode
and strings
functionality:
import (
"fmt"
"os"
"strings"
"unicode"
)
func acronym(s string) (acr string) {
afterSpace := false
for i, e := range s {
if (afterSpace || i == 0) && unicode.IsUpper(e) {
acr += string(e)
}
afterSpace = unicode.IsSpace(e)
}
return acr
}
func main() {
s := "Pan Galactic Gargle Blaster"
if len(os.Args) > 1 {
s = strings.Join(os.Args, " ")
}
fmt.Println(acronym(s))
}
String literals⌗
- the usual
\t
tabs,\n
newline \x61
is a raw byte in hex, evaluating to the charactera
\142
is a raw byte in octal- multi-line literals are supported using backticks
Numbers⌗
- The
bool
type is strictly handled by go, and no auto-type conversion is supported (e.g.if 1
). - Conditions can evaluate a boolean type, a comparison operation or a bool func.
Integers⌗
Go supports integers explicitly; their signage and size.
Signed | Unsigned | Bytes |
---|---|---|
int8 |
uint8 |
1 |
int16 |
uint16 |
2 |
int32 |
uint32 |
4 |
int64 |
uint64 |
8 |
Several aliases to these types exist:
byte
is auint8
rune
is aint32
int
anduint
are CPU platform specific
Arithmetic overflows are silently discarded.
var a uint8 = 250
a = a + 10 // result is larger than 256, so a ends up being 4
Bitwise operations⌗
As the usuals, such as a & b
for bitwise AND, a >> n
right shift by n bits, a &^ b
for a AND NOT b (aka bit clear, all the bits in b set to 1 will be cleared in the result), etc.
// var a uint8 = 64
// uint8 is 8-bits wide, and looks like this at the bit level:
// | 256 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 0 |
// | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
//
// bit shifting to the right 1 bit
// a = a >> 1
// | 256 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 0 |
// | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
var a uint8 = 64
a = a >> 1 //32
Floating point⌗
- Supports IEEE 754 real numbers with the
float32
andfloat64
types. - Prefer
float64
- Decimal fractions cannot be accurately modelled using the binary system. Never use when accuracy is needed, such as for money calculations.
The float binary problem:
$ bc
bc 1.07.1
obase=2
scale=40
1/10
.0001100110011001100110011001100110011001100110011001100110011001100\
110011001100110011001100110011001100110011001100110011001100110011
Constants⌗
Go supports typed and untyped constants, in either the singular or multiple forms.
Go constants can represent these types:
- A unicode rune,
'😀'
'\377'
'\u266B'
'\U0001F600'
- Integers, as decimal
1337
, octal01337
, or hex0x1CAFE42
- Floats
123.456e7
,123e20
,1.234
,.71828
,2.e7
,.5e-10
- Strings, any sequence of unicode runes or escape sequences,
"A string \U0001F600"
- Booleans
true
orfalse
- The
iota
keyword is used to generate enumerations (starting at 0 and incrementing by 1)
Enumerations are possible with the iota
keyword. Reassigning iota
in the same list of consts has no effect.
const kilo = 1024
const pi32 float32 = 3.1415926535897932384
const (
cint = 299792458
ctitle = "Theo de Raadt created OpenBSD in 1995 by forking NetBSD"
)
// all constants are set to 12
const (
twelve = 12
dozen
months
)
const (
zero = iota
one
two
three
four
)
// iota calculation
const (
ten = iota * 10 + 10
twenty
thirty
)
// iota bit shifting
const (
read = 1 << iota // 0001
write // 0010
execute // 0100
isLink // 1000
)
Pointers⌗
- Supports C like
*type
pointer type,&
address of, and*p
pointer indirection - Pointers have a default value of
nil
- Its illegal to point to an arbitrary (literal) address, or a constant
- When
nil
, pointer indirection causes a panic - Can compare pointer addresses
p1 == p2
, or the values they point to*p1 == *p2
- The built-in
new()
allocates variable and returns a pointer to itp := new(int)
var a int = 1337
var p *int = nil
p = &a
fmt.Println("p's value is a's address", p)
fmt.Println("p's value yields a's value", *p)
Result:
p's value yields a's value 1337
p's value is a's address 0xc0000b6010
The new()
operator returns a pointer to an un-named variable:
p := new(int32)
*p = 64
Functions⌗
- Functions are first class objects in Go (i.e. can be assigned to a variable)
- Subsequent parameters of the same type can be grouped
func f(n, m int, s string)
- Named parameters are NOT supported (i.e. must pass in the order specified)
- Variadic functions are supported
func f(s ...string)
- Functions can return multiple values (types must be enclosed in pathentheses)
- Return values can be named, which get declared as variables within the scope of the function
Grouped parameters types⌗
func f(n, m int, s string) {
fmt.Println("A function with grouped parameters:", n, m, s)
}
Variadic functions⌗
func f(s ...string) {
for _, str := range s {
fmt.Print(str + " ")
}
fmt.Println()
}
func main() {
f("clear", "is", "better", "than", "clever")
}
Multiple return values⌗
func f() (int, string, error) {
return 0, "ok", nil
}
n, s, err := f()
Named return values⌗
func f() (n int, s string, err error) {
n = 5
s = "high five!"
return // Don't do this - it works, but it can be confusing
}
Recursion⌗
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n - 1)
}
Deferred functional calls⌗
Using defer
will “queue” a function call until the point where the calling function itself exits.
This is particularly useful in simplifying a function that has multiple exit points, needing to perform the same resource cleanup (e.g. f.Close()
) at all return points.
Instead, just ensure it gets done once at the functions end with defer
.
func f() error {
f, err := os.Open("foo.go")
if err != nil {
return err
}
defer f.Close()
fmt.Println(f.Name())
if f.Name() == "foo.go" {
return nil
}
// More code, maybe more exit points
return nil
}
Functions as values⌗
Functions being first class objects, means you can throw them around like variables.
Note the funcVar
declaration below, there is no need to name parameters.
func f1(s string) bool {
return len(s) > 0
}
func f2(s string) bool {
return len(s) < 4
}
var funcVar func(string) bool
func main() {
funcVar = f1
fmt.Println(funcVar("abcd"))
funcVar = f2
fmt.Println(funcVar("abcd"))
}
Function literals (anonymous functions)⌗
funcVar = func(s string) bool {
return len(s) > 4
}
Its possible to evaluate the function literal after defining it, which can be useful when creating goroutines
in a loop:
var result string = func() string {
return "abcd"
}()
Passing functions to functions⌗
As first class objects, in addition to being treated as variables, this translates over to function parameters in the same way.
func f1(s string) bool {
return len(s) > 0
}
func f2(s string) bool {
return len(s) < 4
}
func funcAsParam(s string, f func(string) bool) bool {
return f(s + "abcd")
}
func main() {
fmt.Println(funcAsParam("abcd", f1))
}
Closures⌗
When a function literal is defined within another function.
Closures get access to the local variables (i.e. the call stack) of the outer function, even after the lifetime of the outer function.
func newClosure() func() {
var a int
return func() {
fmt.Println(a)
a++
}
}
c := newClosure()
c()
c()
c()
Error Handling⌗
- Unlike other langs, exceptions are NOT supported.
- Go takes a different standpoint, error handling is instead part of normal control flow logic
- Expected errors are simply returned to the caller (Go functions can return multiple things, its convention for
error
to be the last return value of functions) - Caller should always check each returned
error
before moving forward - Error handling strategies: propagate, retry, log and continue, or log and exit
- A
panic
unlike anerror
, are used to communicate exceptional errors or serious failures - A
panic
results in a controlled crash
Error handling strategies⌗
Propagate to caller⌗
- while the raw
error
can simply be returned, its best practice to wrap a new error message prior to propagating - avoid newline characters in messages for grep-ability.
func propagate(i int) error {
if err := verify(i); err != nil {
return fmt.Errorf("propagate: %s", err)
}
return nil
}
Retry⌗
For transient errors, like a failing network link.
- wait a time period before retrying
- try a different port, file name, IP, reduced size, etc
- fallback to default values
func retry(i int) error {
err := propagate(i)
if err != nil {
err = propagate(i / 2)
if err != nil {
return fmt.Errorf("retry: %s", err)
}
}
return nil
}
Log and continue⌗
The error
is no significant enough to disturb control flow. Log it and move on.
func onlyLog(i int) {
if err := retry(i); err != nil {
log.Println("onlyLog:", err)
}
}
Log and exit⌗
The error
is a show stopper. The log
package conveniently has log.Fatal()
, log.Fatalln()
and log.Fatalf()
, which print and exit.
pkg/errors⌗
pkg/errors makes it easy to account for each error
and its context, as errors continue to be propagated and wrapped up the call chain. By providing a simple API errors.Wrap()
to wrap errors with call stack context before propagating, errors.WithStack()
to print full list of error messages
_, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "read failed")
}
Panic and Recover⌗
Pull the ejector seat; something unpredictable happened or the caller passed invalid arguments or calling context.
func unexpectedError(p *int) {
if p == nil {
panic("p must not be nil")
}
}
func main() {
unexpectedError(nil)
}
Packages and Libraries⌗
Go’s namespace concept.
- The
main
package represents an executable - Any other package represents a library
- A library can contain functions, types, variables, but NO
main
function - Libraries are a great way to break up complexity into chunks
- They are imported
import "mastergolib/dice"
. File names of imports are irrelevant. - A simple visibility rule applies for package, a function/variable/type that starts with an uppercase letter is visible to outside users of the package.
- If a package needs some initialisation logic to fire at startup, use the
init()
hook.
func init() {
rand.Seed(time.Now().UnixNano())
}
Package aliases⌗
- Occassionally you will need to use libraries with same name, and alias can help un-ambiguise this
- Nice way to cut down long package names
import "time"
import tm "github.com/someuser/time"
Imported unused packages for side effects⌗
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
Inspecting a package API⌗
Say you’ve just pulled down a nice new package to mess with go get github.com/appliedgocourses/bank
.
Use go doc
to inspect the API like so (getting more specific with path details, if you have multiple packages with the same name).
To get an API overview:
go doc bank
go doc appliedgocourses/bank
go doc github.com/appliedgocourses/bank
To drill down to a specific function:
go doc bank.Balance
Advanced Data Types⌗
Arrays⌗
- As expected, are fixed in size and type.
- Unusually arrays in Go are treated as value types (i.e. not by reference). Full copies are made when assigning to another variable, or passing to a function.
- Use array pointers (or slices) to overcome this expensive for large arrays.
- For this reason, in Go, it is more idiomatic to use slices
In Go, the type always goes on the end:
var name [10]string
var size [1024]int
The contents of the array can be immediately assigned (composite literal):
// set each element
a := [...]int{1, 2, 3, 4, 5}
// selectively set elements by index
a := [10]string{0: "First", 9: "Last"}
Accessing array elements:
i := 10
a := [10]string{0: "First", 9: "Last"}
b := a[3]
fmt.Printf("%#v\n", a[i]) // runtime panic
// fmt.Printf("%#v\n", a[10]) // compile time error
Iterating with range
, which return the current index
and value
. Remember range
returns a COPY of each element. If you want to mutate the array, use the index
(not the value
):
list := [...]int{1, 2, 3}
for i := range list {
list[i] = 4
fmt.Println(list[i])
}
fmt.Println(list)
Array pointers, do NOT need to be dereferenced:
pa := &[3]bool{true, false, true}
b := (*pa)[0] // no need to dereference
b := pa[0] // a vanilla array value expression just works
Comparing arrays, works if the type it holds is comparable:
a1 := [3]string{"one", "two", "three"}
a2 := [3]string{"one", "two", "three"}
fmt.Println("a1 == a2:", a1 == a2)
2D arrays (matrices):
var matrix [4][4]int
for i := range matrix {
for j := range matrix[i] {
matrix[i][j] = (i + 1) * (j + 1)
}
}
fmt.Println(matrix)
Slices⌗
Unique to Go, slices are more versatile than arrays and happen to use arrays internally.
- Unlike arrays are reference based (i.e. use pointers to the underlying array) and can even be extended! 💥
- Use Go built-in functions such as
len()
,cap()
,append()
copy()
- As a result are cheaper to pass around
- Multiple slices can overlap with each other, all pointing to the same underlying array (i.e. mutating one slice can affect another)
- Internally a slices have a header that defines:
- current length (as shown by
len()
) - remaining capacity in the internal array (as shown by
cap()
) - pointer to the data in the internal array
- current length (as shown by
- Slices can be based of unnamed arrays created with
make()
!IMPORTANT!
Please grok the following about slices, or risk insanity:
- Go’s pass by value semantics does NOT support deep copying.
- The means when you assign a slice variable to another, or pass a slice into a function call, Go will (shallow) copy it.
- The 3 components (
len
,cap
andaddress
) of the slice header will be copied to another slice header. - The internal array used for the slices however will not be copied, and all copied slices will continue point to the same array in memory.
Examples help amirite?
- Once upon a time there was a slice
a := []int{0, 0, 0, 0}
- Internally for
a
Go made this slice header&{Data:824633819968 Len:4 Cap:4}
- You innocently pass
a
into a functionfunc appendOne(s []int) { ...
- Because Go does NOT do pass by reference, Go will create a (shallow) copy of
a
fors
. - Now slice
s
has its own slice header, copied froma
’s slice header i.e.&{Data:824633819968 Len:4 Cap:4}
- Now penny drop moment.
- Notice the pointer address to the internal array of the slice. Go lazily just kept the same address of
a
’s array, instead of copying the internal array ofa
to a new memory location, and setting the pointer address to this new array fors
. - If it did do the later, this is known as a DEEP COPY.
- Go does NOT do deep copies.
- Now if you set out and start updating the slices
a
ands
, even though they are copies, their internal array pointers still point to the same memory. - In other words they will update the same data.
This can get even more wacky:
- The slices copied from the same origin can drift apart. For example, if you extend slice
a
beyond its capacity, Go will reallocate it’s internal array to a new array double the size, and update its slice header with the new size, capacity and pointer to the new larger array. - However slice
s
will continue to point to the old smaller array memory location. - HOLY MOLY. This could create some wacky bugs.
a := [8]int{} // dumb array
sa := a[3:7] // slice
sb := a[:4] // slice, from first element to index
sc := a[4:] // slice, from index to last element
Makes a slice from array a
, from element 3 up to, but not including, element 7. Weird, like strings. Use the boundary trick to help remember this oddity.
Extending slices⌗
Option 1: re-slice the slice
a := [8]int{}
s := a[3:6] // len(s) == 3, cap(s) == 5
s = s[:1] // len(s) == 2, cap(s) == 5
s = s[:cap(s)] // len(s) == 5, cap(s) == 5
Option 2: append the slice
Append arbitrary new elements to slice, even beyond its original capacity. Go will allocate a brand new array twice the size, if capacity is exceeded.
Dag alert: append()
returns a new slice due to Go’s pass by value semantics.
s = append(s, 1, 2, 3, 4)
Creating a slice with make⌗
A slice definition looks similar to an array, except no size is specified:
var s []int // a slice (not an array!)
Instantiating a slice (3 options):
// option 1: slice literals
s = []int{}
s = []int{1, 2, 3}
// option 2: make
s := make([]int, 10, 100)
// option 3: append() a nil slice
append(s, 1, 2, 3)
Byte slices⌗
A most popular slice type, as they can be used as mutable strings.
- Casting between strings and byte slices is easy.
- Slices can mutate data in they underlying array, diverging from Go’s usual pass by value semantics.
range
on byte slices does not recognise Unicode runes (i.e. each piece of data is treated as an individual byte)- Byte slices can’t be initialised with a literal e.g.
var coolString []byte = "smells like teen spirit" //compile error
- Instead, type casting must be used e.g.
var coolString []byte = []byte("smells like teen spirit")
- The built-in bytes package offers a ton a byte slice, almost mirroring what can be done with the strings package
Casting examples:
s := "Eat 200 grams of protein daily"
b := []byte(s)
s = string(b)
Maps⌗
The classic key/value based data structure.
- They are mutable, and offering all the CRUD operations.
- As with slices, Go’s pass by value semantics does NOT support deep copying. See !IMPORTANT! in the slices section above.
- Maps internally point to an internal hash table data structure (the same way slices point to an internal array)
- All keys must be of the same type (ex:
string
), and must be comparible==
. - All values must be of the same type (ex:
int
) - Elements of a map are not externally addressable, so no pointers to individual elements.
- Although Go does provide a
Set
type (a data structure where every value is unique), maps can easily simulate onevar set map[string]struct{}
. An empty custom structure
Creating maps⌗
By default is nil
:
var m map[string]bool
fmt.Println("m is nil:", m == nil) // true
To initialise, two options, use make()
or a composite literal.
Using make()
:
m := make(map[string]int)
m := make(map[string]int, 100) // pre-allocate space for 100 elements
Using a composite literal:
moons := map[string]int{
"Earth": 1,
"Mars": 2,
"Jupiter": 67,
"Saturn": 62,
}
CRUD (create retrieve update delete) operations with maps⌗
Creating:
moons := map[string]int{
"Earth": 1,
"Mars": 2,
"Jupiter": 67,
"Saturn": 62,
}
moons["Uranus"] = 27
moons["Neptune"] = 14
moons["Pluto"] = 5
Retrieving:
mercuryMoons := moons["Mercury"]
fmt.Println("Mercury has", mercuryMoons, "moons.")
To test existance, use the comma, ok idiom:
_, ok := moons["Venus"]
if !ok {
fmt.Println("Data for Venus does not exist")
}
Iterate with range
of course:
for k, v := range moons {
fmt.Println("Planet", k, "has", v, "moons.")
}
Updating:
moons["Jupiter"] = moons["Jupiter"] - 1
moons["Jupiter"]++
Deleting:
delete(moons, "Pluto") // Pluto is not a regular planet
Named types (user defined types)⌗
Your own types based on other primitive types.
- Defined using
type
keyword - Can cast into other types, if they are of the same underlying type
- Its possible to add custom methods to these types
- Print types using
fmt
and%T
- Functions, like primitive types, can be with named types
type km float64
type miles float64
func howLongDoINeedToWalk(distance km) {
fmt.Println("You need to walk", distance/5.0*60, "minutes for", distance, "km.")
}
func main() {
var dst km = 12
howLongDoINeedToWalk(dst)
}
Function named type⌗
type action func(int) int
func double(n int) int {
return n *2
}
func apply(change action, n []int) {
for i := range n {
n[i] = change(n[i])
}
}
func main() {
ints := []int{1,2,3,4,5}
apply(double, ints)
fmt.Println(ints)
}
Type aliases⌗
Tells Go to bind an identifier to a given type.
Great way for libraries to insulate external users of concrete types.
type FooType = oldlib.FooType
Struct⌗
A way of structuring data together.
- Is a data type, like a named type, declared using the
type
keyword - The same visibility rules of packages apply (i.e. if field identifier starts with upper or lower character)
- Are comparable, if each field type is in-turn comparable (
earth == jupiter
) - Can use dot accessors immediately on functions that return a
struct
ex:uppercase(mars).Name
type Planet struct {
Name string
Mass int64
Diameter int
Gravity float64
RotationPeriod time.Duration
HasAtmosphere bool
HasMagneticField bool
Satellites []string
next, previous *Planet
}
var earth, jupiter Planet
Supports initialisation with a literals.
- Make sure to always set the final trailing comma when using multiline syntax
- Specific fields can be left out (they will get their default values)
- Field names can be omitted entirely. In this case, all values must be included in the same order as they are declared
mars := Planet{
Name: "Mars",
Diameter: 6792,
RotationPeriod: 24.7 * 60 * time.Minute,
HasAtmosphere: true,
Satellites: []string{"Phobos", "Deimos"},
Mass: 642e15, // in millon metric tons (1t == 1000kg)
previous: &earth,
next: &jupiter, // Remember the final comma
}
Struct embedding⌗
Yes, structs can be embedded within one another.
- Embedding offers a lightweight inheritance. In OOP inheritance is a way of sharing behavior between classes of objects.
- When a struct is embedded into another struct, the outer struct has access to the methods of the embedded struct!
type CelestialBody struct {
Name string
Mass int64
Diameter int64
}
type Planet struct {
CelestialBody // Anonymous field: No name, only a type
HasAtmosphere bool
HasMagneticField bool
Satellites []string
next, previous *Planet
}
var p Planet
p.Name = "Venus"
Field tags⌗
A string literal that can be added to a struct field.
- For marking up metadata about fields
- Popular use-cases include code generators, ORM’s, serializers.
- For example, the
encoding/json
package, when marshalling data, processes all field tags starting withjson:
, allowing field renamings to occur - Hot tip: wrapping field tags in backticks, saves having to escape special printable characters such as double quotes
type weatherData struct {
LocationName string `json:"location"`
Weather string `json:"weather"`
Temperature int `json:"temp"`
Celsius bool `json:"celsius,omitempty"`
TempForecast []int `json:"temp_forecast"`
}
Struct methods⌗
In Go, functions can be bound to types, commonly known as methods in OOP.
- A method, similar to a function, but uniquely defines a receiver data type, ex:
func (p Person) Age() int {
- The receiver is a typed parameter that is put in front of the function name, within parentheses
- The type of the receiver is must be a base type (i.e. NOT an instance of a pointer or interface) that exists within the same package
- Like the rest of Go, pass-by-value semantics apply to method receivers.
- As a result, its NOT possible to update a receiver value directly, a pointer receiver must be used.
- If you want to base a type on a struct that has methods ex:
type Contact Person
, methods will not flow toContact
, as they have a receiver type ofPerson
. Bummer. - Instead, use type embedding or type aliases (
type Contact = Person
).
type Person struct {
Name string // exported
dateOfBirth time.Time // internal
}
func (p Person) Age() int {
return int(time.Since(p.dateOfBirth).Hours() / 24 / 365)
^-- accessing the receiver
}
Pointer receivers, allow methods to mutate state in the object (ex: a setter method):
func (p *Person) ChangeName(name string) {
p.Name = name // p.Name is a shorthand for (*p).Name
}
Receiver Method Sets⌗
All the methods for a type (T
) that have a matching receiver type.
type Counter int
func (c *Counter) Up() {
(*c)++
}
func (c *Counter) Reset() {
*c = 0
}
func (c Counter) Get() int {
return int(c)
}
In the above, the method set for Counter
includes just the Get()
method. The Up()
and Reset()
methods are of a pointer receiver type (*Counter
), and therefore are not part of Counter
method set.
- The method set of a type
T
consists of all methods with a receiver typeT
- The method set of a type
*T
consists of all methods with a receiver type*T
plus all methods with a receiver type ofT
.
Interfaces⌗
- Defines one or more function signatures, but does NOT implement them
- In Go, a type implicitly (automatically) satisfies an interface by implementing all the functions defined by an interface
- Yes Java peeps, there is NO need to explicitly declare that an interface is implemented.
- Be careful when using interfaces with types that have methods with pointer receivers
- The compiler will stop you, with Invitation does not implement Helloer (Hello method has pointer receiver)
- In
h = Invitation { event: "hackathon" }
,h
is of typeInvitation
, whose method set does NOT include any methods for*Invitation
(pointer receiver). - Quick fix is easy, just assign the interface to a pointer of the type
h = &Invitation{event: "birthday party"}
- Interfaces can build upon other interfaces, such as the
ReaderWriter
interface from the standard library. - All types in Go satisfy the empty interface
interface {}
, can be handy in the absence of generics. - Other cool standard lib examples include the
bufio
package,error
which is just an interface, thesort.Interface
.
Two concrete Helloer
implementations:
type Helloer interface {
Hello(string)
}
type Greeting string
func (g Greeting) Hello(name string) {
fmt.Println(g+",", name)
}
type Invitation struct {
event string
}
func (inv Invitation) Hello(name string) {
fmt.Printf("Welcome to my %s, %s! Please on come in\n", inv.event, name)
}
func Main() {
var h Helloer
h = Greeting("G'day")
h.Hello("Benjamin")
h = Invitation { event: "hackathon" }
h.Hello("Benjamin")
}
Interfaces support:
- Decoupling concrete implementations from consumers
- Mocking out implementation for unit testing
- Changing complete implementations over time
For example, the below doSomething
could take in a net.Conn
, an *os.File*
or a *strings.Reader
, all of which implement the interface of io.Reader
.
func doSomething(in io.Reader) {
}
The List
type below implements the sort.Interface
contract, giving it sort powers:
type List []string
var names List
func (l List) Len() int {
return len(l)
}
func (l List) Less(i, j int) bool {
return len(l[i]) < len(l[j])
}
func (l List) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}
func main() {
list := List{"really really long", "short", "quite long", "longer"}
sort.Sort(list)
fmt.Printf("%#v\n", list)
}
Interface internals⌗
To represent an interface, Go stores its type descriptor (the concrete type that implements the interface) and value descriptor (a pointer to the concrete instance).
For example:
- Declaring a unassigned interface
var w io.Writer
, its type isnil
, its value isnil
- Assigning it to an instance that satisfies the contact
w = io.Stdout
, the type is set to*os.File
, its value set to the memory location of its instance0xdeadbeef
WARNING - dont assign nil variables to interfaces⌗
Take the following:
var w io.Writer
var f *os.File
w = f
This will set the type descriptor of w
to *os.File
, but its value descriptor to nil
.
Attempting to nil
test the interface will pass, however using it will fail.
Maddening times.
Type assertion⌗
Turns a looser type into its concrete type. If the assertion fails, it will panic.
func doSomething(in io.Reader) {
conn, ok := in.(net.Conn)
if ok {
fmt.Println("Read from remote address", conn.RemoteAddr())
}
}
Type switches⌗
Exposes underlying type:
switch stream := in.(type) {
case net.Conn:
fmt.Println("Read from remote address", stream.RemoteAddr())
case *os.File:
fmt.Println("Read from file", stream.Name())
case *strings.Reader:
fmt.Println("Read from a string of length", stream.Len())
}
Cool cool cool⌗
stdlib⌗
The Go standard library is a thing of beauty, pragmatic and brilliant documentation.
Gems:
- gob streams for exchanging binary between an
Encoder
andDecoder
. Handy for sending or receiving objects to a remote machine or a blob store.
Tools⌗
- wuzz TUI REST client
Make⌗
Vim setup⌗
- Install the vim-go and coc.nvim plugins. I use neovim with vimplug to do this.
- Setup
GOPATH
andGOBIN
env. - Run
vim +GoInstallBinaries
to install supporting toolchain inGOBIN
. - Setup coc config by running
:CocConfig
within vim. Ensure the language server is setup for go, using the provided snippet. - Jump into some go code such as vmware-tanzu/velero, and test
gd
goto def, andctrl o
to bounce back. Cursor over some func and hit:GoDoc
to show contextual docs. - Congrats, you have tight integration with go toolchain thanks to
vim-go
, but also modern language intelligence thanks to LSP,gopls
andcoc-nvim
.
Libraries⌗
Data⌗
- BoltDB transactional key-value store, both as byte slices. Organised in buckets.
Middleware⌗
Web⌗
- gin HTTP web framework
- go-jose implements the Javascript Object Signing and Encryption set of standards JWE, JWS and JWT (JSON Web Encryption, JSON Web Signature and JSON Web Token)
- gorilla a general purpose web toolkit that solves doing context, routing, RPC over HTTP, strong typing forms to structs, secure cookies, session and websockets.