golang
Tags :: Language
Install
See site if not on mac w/ homebrew, otherwise
brew install go
Access all tools with go
.
- compiler:
go build
- code formatter:
go fmt
- dependency manager:
go mod
- test runner:
go test
- mistake scanner:
go vet
Hello world
Go code runs in modules.
Initialize module
Within a new directory, initialize a module with
go mod init your/module
This will create a new file go.mod
First program
Code is located in main.go
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
Run in the same directory as main.go
with
go run main.go
Types
Type | Category | Description |
---|---|---|
Zero | Predeclared | Default value assigned to declared but unassigned variables |
Literals | Predeclared | Explicitly specified values |
Integer | Literals | Base 10 by default |
Floating-point | Literals | Uses decimal point for fractions |
Rune | Literals | Single character representation |
String | Literals | Two creation methods |
Booleans | Predeclared | Binary true/false values |
byte | Integer | Alias for uint8 |
int | Integer | Signed integer sized to architecture’s word size |
uint | Integer | Unsigned integer sized to architecture’s word size |
rune | Integer | Alias for int32 |
uintptr | Integer | Integer type for storing pointer values |
String | Predeclared | Immutable sequence of bytes, zero value is empty string |
Type conversion
Go does not support automatic type conversion. All variable types need to be converted to interact.
var x int = 10
var y float64 = 30.2
var sum1 float64 = float64(x) + y
var sum2 int = x + int(y)
fmt.Println(sum1, sum2) // 40.2 40
Slices
Core Concepts
- Zero value:
nil
- Only comparable with
nil
and==
- Modifying shared slices affects all references
- Length can differ from capacity
Common Operations
Creation
var s []int // nil slice
s := []int{1, 2, 3} // slice literal
s := make([]int, 5) // length 5, capacity 5
s := make([]int, 0, 10) // length 0, capacity 10
Modification
s = append(s, 10) // single element
s = append(s, 1, 2, 3) // multiple elements
s = append(s, other...) // append slice
clear(s) // zero all elements (Go 1.21+)
Slicing
s[low:high] // items low through high-1
s[low:] // items low through end
s[:high] // items beginning through high-1
s[:] // copy of entire slice
s[low:high:max] // control capacity of new slice
Copying
copy(dest, src) // copies min(len(dest), len(src)) elements
copy(dest, src[1:]) // copy from middle of src
Array Conversion
array := [4]int{1, 2, 3, 4}
slice := array[:] // array to slice
array2 := [4]int(slice) // slice to array (new memory)
arrayPtr := (*[4]int)(slice) // slice to array pointer (shared memory)
Best Practices
When to Use What
var
declaration (nil slice): When size unknown, no initial data- Slice literal: When starting values are known
make
:- With length: For buffers or exact size known
- Zero length + capacity: For growing slices
Memory Management
- Capacity doubles when < 256
- Growth slows to ~25% for larger slices
- Use full slice expression
s[low:high:max]
to prevent capacity issues - Be careful with shared memory when slicing
Standard Library
slices.Equal(a, b)
: Compare if length and elements matchslices.EqualFunc(a, b, fn)
: Custom comparison function
Maps
Maps are declared as map[keyType]valueType
. A nil
map (e.g., var m map[string]int
) cannot be written to. Use make
or a map literal for writable maps.
// Map literal
m := map[string]int{"Alice": 10, "Bob": 20}
// Using make
m := make(map[string]int, 10) // Capacity 10
m["Charlie"] = 30
Structs
Structs are user-defined types with fields. Define them with type
and access fields using dot notation.
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 30}
fmt.Println(p.Name)
Anonymous structs are inline struct definitions:
pet := struct {
Name string
Kind string
}{Name: "Fido", Kind: "dog"}
Pointers
Use &
to get a pointer and *
to dereference. nil
pointers cannot be dereferenced.
x := 10
p := &x
fmt.Println(*p) // 10
Shadowing
Inner-scope variables can shadow outer-scope ones:
x := 10
if x > 5 {
x := 5
fmt.Println(x) // 5
}
fmt.Println(x) // 10
Control Flow
if
Go’s if
statements do not require parentheses. You can declare variables scoped to the condition:
if n := rand.Intn(10); n > 5 {
fmt.Println("Big:", n)
} else {
fmt.Println("Small:", n)
}
for
The for
loop supports:
-
Complete (C-style):
- Initialization, condition, increment:
for i := 0; i < 10; i++ { fmt.Println(i) }
-
Condition only:
- Equivalent to
while
:
i := 1 for i < 10 { fmt.Println(i) i++ }
- Equivalent to
-
Infinite:
for { fmt.Println("Infinite loop") }
-
For-range:
- Iterate over collections:
nums := []int{1, 2, 3} for i, v := range nums { fmt.Println(i, v) }
If you don’t need the key/index in, the underscore (_) can be used, telling go to ignore the value.
evenVals := []int{2, 4, 6, 8, 10, 12} for _, v := range evenVals { fmt.Printf("%d ", v) }
If you just need the key/index, you can leave off the second variable:
uniqueNames := map[string]bool{"Fred": true, "Raul": true, "Wilma": true} for k := range uniqueNames { fmt.Printf("%s ", k) }
switch
words := []string{"a", "cow", "smile", "gopher",
"octopus", "anthropologist"}
for _, word := range words {
switch size := len(word); size {
case 1, 2, 3, 4:
fmt.Println(word, "is a short word!")
case 5:
wordLen := len(word)
fmt.Println(word, "is exactly the right length:", wordLen)
case 6, 7, 8, 9:
default:
fmt.Println(word, "is a long word!")
}
}
Functions
Variadic Parameters
Variadic functions accept a variable number of arguments as a slice:
func addTo(base int, vals ...int) []int {
out := make([]int, 0, len(vals))
for _, v := range vals {
out = append(out, base+v)
}
return out
}
Multiple Return Values
Functions can return multiple values, often used with errors:
func divAndRemainder(num, denom int) (int, int, error) {
if denom == 0 {
return 0, 0, errors.New("cannot divide by zero")
}
return num / denom, num % denom, nil
}
named return values
Functions can have names specified for the return values, which predeclares variables that are used within the function to hold the return values. These values are initialized to their zero values allowing you to return them before any explicit use or assignment.
func divAndRemainder(num, denom int) (result int, remainder int, err error) {
if denom == 0 {
err = errors.New("cannot divide by zero")
return result, remainder, err
}
result, remainder = num / denom, num % denom
return result, remainder, err
}
Advanced Concepts
Closures
These are functions defined within a function, and are able to access and modify variables declared in the outer function. Closures allow you to pass fome function state to another function. These can also be returned from a function.
package main
import "fmt"
func main() {
a := 20
f := func() {
fmt.Println(a)
a = 30
}
f()
fmt.Println(a)
}
Anonymous function f
can read and write a
even though a
is not passed in to the function. Using :=
instead of =
inside the closure creates a new a
that ceases to exist when the closure exits.
Why use closures?
-
limit function scope
If a function is going to be called from only one other function, but it’s called multiple times, you can use an inner function to “hide” the called function. This reduces the number of declarations at a package level, making it easier to find an unused name.
-
removing logic repetetion
Given a piece of logic that is repeated multiple times within a function, a closure can be used to remove that repetition. See the following example from a jonbodner/my_lisp:
package scanner import "github.com/jonbodner/my_lisp/types" func Scan(s string) ([]types.Token, int) { var out []types.Token var curTokenTxt []rune buildCurToken := func() { if len(curTokenTxt) > 0 { if len(curTokenTxt) == 1 && curTokenTxt[0] == '.' { out = append(out, types.DOT) } else { out = append(out, types.NAME(curTokenTxt)) } curTokenTxt = make([]rune, 0) } } update := func(t types.Token) { buildCurToken() out = append(out, t) } depth := 0 for _, c := range s { switch c { case '(': update(types.LPAREN) depth++ case ')': update(types.RPAREN) depth-- case '.': curTokenTxt = append(curTokenTxt, c) case '\n', '\r', '\t', ' ': buildCurToken() case '\'': update(types.QUOTE) default: curTokenTxt = append(curTokenTxt, c) } } buildCurToken() return out, depth }
Here the scanner uses two closures:
buildCurToken
andupdate
to make the code shorter and easier1 to understand.
-
passing to funcs/returning from funcs
These become really interesting, allowing you to take variables within your function and use those variables outside of the function
Passing and returning funcs from funcs
Passing
Functions are values and the type of a function is specified using its parameter and return types, thus you can pass functions as parameters into functions.
NOTE: Think about the implications of creating a closure that references local variables and then passing that closure to another function. It means a closure can be used to pass some function state to another function.
An example of this pattern is when sorting slices with the stdlib sort.Slice
, which takes in any slice and a function that is used to sort the slice that’s passed in. See the following with a simple type, Person
, that is sorted first by last name and then by age:
package main
import (
"fmt"
"sort"
)
func main() {
type Person struct {
FirstName string
LastName string
Age int
}
people := []Person{ //
{"Pat", "Patterson", 37},
{"Tracy", "Bobdaughter", 23},
{"Fred", "Fredson", 18},
}
fmt.Println(people)
// sort by last name
sort.Slice(people, func(i int, j int) bool { //
return people[i].LastName < people[j].LastName
})
fmt.Println(people)
// sort by age
sort.Slice(people, func(i int, j int) bool { //
return people[i].Age < people[j].Age
})
fmt.Println(people)
}
- First a slice, people, of type
Person
is created. - people is then sorted by last name and the results printed
- Another closure is defined to sort by
Age
following the same practice as before. - The original people slice is changed by the call to
sort.Slice
Returning
A closure can also be returned from a function. See the following which returns a mutliplier function.
package main
import "fmt"
func makeMult(base int) func(int) int {
return func(factor int) int {
return base * factor
}
}
func main() {
twoBase := makeMult(2)
threeBase := makeMult(3)
for i := 0; i < 3; i++ {
fmt.Println(twoBase(i), threeBase(i))
}
}
Returning closures is used for a variaty cases where closures are useful. Such as sorting and and searching slices. For returning closures useful examples include middleware implementations and resource cleanup with defer
.
NOTE: Functional languages such as haskell use the terminology higher-order functions to refer to a function that has a function for an input parameter or return value.
defer
Quick notes:
- can use function, method, or closure
- can
defer
multiple functions within a function- Runs in LIFO order; the last
defer
registered runs first.
- Runs in LIFO order; the last
- code within
defer
functions runs after the return statement.- input parameters supplied to
defer
are evaluated immediatly and their values are stored until the function runs.package main import "fmt" func deferExample() int { a := 10 defer func(val int) { fmt.Println("first:", val) }(a) a = 20 defer func(val int) { fmt.Println("second:", val) }(a) a = 30 fmt.Println("exiting:", a) return a } func main() { _ = deferExample() }
- input parameters supplied to
Cleanup code is attached to a the function with the defer
keyword. See the following simple version of cat.
package main
import (
"io"
"log"
"os"
)
func main() {
if len(os.Args) < 2 {
log.Fatal("no file specified")
}
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer f.Close() //
data := make([]byte, 2048)
for {
count, err := f.Read(data)
os.Stdout.Write(data[:count])
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
break
}
}
}
The important thing to see in this example is using defer to close the file handle. Once there is a valid file handle, it needs to be closed after it is used, no matter how the function is exited. defer
ensurs that the cleanup code runs. It is used with the defer
keyword, followed by a function or method call. Normally a function call will run immediatly but defer
delays invocation until the surrounding function exits.
-
Common pattern to modify values in surrounding function
Deferred functions can examine or modify the return values of the surrounding functio. By using named return values, it can allow your code to take actions based on an error. See the following database example:
func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string) (err error) { tx, err := db.BeginTx(ctx, nil) if err != nil { return err } defer func() { // if err == nil { err = tx.Commit() } if err != nil { tx.Rollback() } }() // compile time error to leave out the braces _, err = tx.ExecContex(ctx, "INSERT INTO FOO (val) values $1", value1) if err != nil { return err } // use tx to do more database inserts here return nil }
If any of the transactions fail you want to rollback the modifications, and if any succed you want to commit the changes. The closure with defer checks if
err
has been defined a value. If it hasn’t you runtx.Commit
, which could al return an err. If it does the valueerr
is modified. If any database interaction returnedtx.Rollback
is called.
-
Common pattern for resource allocation
Take the cat program. It can be rewritten to use a helper function that opens a file and returns a closure.
func getFile(name string) (*os.File, func(), error) { file, err := os.Open(name) if err != nil { return nil, nil, err } return file, func() { file.Close() }, nil }
The helper returns a file, function, and an error. From
main
we can use the helper like so:f, closer, err := getFile(os.Args[1]) if err != nil { log.Fatal(err) } defer closer()
Since go doesn’t allow unused variables, returning the
closer
from the function means that the program will not compile if the function is not called, reminding the user to use defer.
Pointers
Standard address (&
) and indirection/dereferencing (*
) operators. Pointer tricks such as pointer arithmetic that you can do in C are not allowed in go.
Before dereferencing a pointer, you must make sure it is non-nil, otherwise the program will panic if you attempt to dereference a nil
pointer:
var * int
fmt.Println(x == nil) // prints true
fmt.Println(*x) // panics
The pointer type can be based on any type, and the built in function new
creates a pointer variable which returns a pointer to a zero-value instance of the type.
var x = new(int)
fmt.Println(x == nil) // prints false
fmt.Println(*x) // prints 0
However, the new
function is rarely used.
For structs use &
before a struct literal to create a pointer instance. When you need a pointer to a primitive type, declare a variable and then point to it.
pointers of constants / literals
Constant literals cannot use &
because they don’t have a memory address, e.g.
type person struct {
FirstName string
MiddleName *string
LastName string
}
p := person {
FirstName: "Pat",
MiddleName: "Perry", // This line will not compile
LastName: "Peterson",
}
There are two ways around the problem. The first is to introduce a variable to hold the constant. The second is to write a generic helper function that takes a parameter of any type and returns a pointer to that type:
func makePointer[T any](t T) *T {
return &t
}
which allows one to write the previous code as
p := person {
FirstName: "Pat",
MiddleName: makePointer("Perry"),
LastName: "Peterson",
}
NOTE: The only time you should use pointer parameters to modify a variable is when the function expects an interface, e.g. JSON.
Difference between maps and slices
Within the go runtime, a map is implemented as a pointer to a struct, so passing a map to a function means that you are copying a pointer. Thus changes made to a map within the functions are reflected in the original.
NOTE: maps for input parameters or return values should be carefully considered. On an API design level, they are a bad choice because they say nothing about the values contained within. In general one should prefer a struct for these cases.
A slice is more complicated. Any modification to a slices contents within a function is reflected in the original, but using append to change the length of the array is not, even if the slice has a capacity greater than its length. This is due to slice being implemented a struct with three fields:
int
for lengthint
for capacity- a pointer to a block of memory
Reference
-
Does it make it easier to track the logic? I guess it depends on experience. Should try to find a writeup discussing this. ↩︎