Below are some notes on the Go language, for my personal reference.
Disclaimer: Most of the examples are adapted from Go’s official guide - A Tour of Go.
Structure
- Repository —> Module(s) —> Package(s)
- Package
- is a collection of source files
- compiled together
- definitions (functions, types, variables, constants) are visible within package
- Module
- is a collection of one or more related packages, specifically all packages contained in module root and subdirectories (unless it contains another
go.mod
file) - released together
- defined by a
go.mod
file at module root; which declares the module path, the import path prefix for all packages within that module - the module path also serves as the download path
- is a collection of one or more related packages, specifically all packages contained in module root and subdirectories (unless it contains another
- Repository
- is a collection of one (typically) or more modules
- typically: repository root = module root
Get started
1. Install
on MacOS :
$ brew install go
Optional: Change Go workspace location
By default, Go will install packages to $HOME/go
.
Set a different path, by setting the GOPATH
environment variable in your shell configuration (bash_profile
in my case):
# Setting workspace path for Go
export GOPATH=${HOME}/your/go/workspace
2. Create a HelloWorld module
$ mkdir HelloWorld
$ cd HelloWorld/
$ go mod init example.com/go/helloworld
go: creating new go.mod: module example.com/go/helloworld
3. Create your first Go file
- create the
.go
source file
$ touch hello.go
- The first line contains the package name. Assign it to package
main
, because executable files must always usemain
.
package main
- Add some code
package main
import "fmt"
func main() {
fmt.Printf("hello, world\n")
}
Language and Control Structures
Package
// package <name>
// name "main" for executable package
package main
// import statements
import "fmt"
import "math"
// equivalent factored import statements (suggested style)
import (
"fmt"
"math"
)
func main() {
fmt.Printf("Now you have %g problems.\n", math.Sqrt(7))
}
Exports
Any function, constant or variable with a capital name is exported from the package. All others are not exported and therefore not accessible outside the package.
Variables and Constants
Variable
// declarations (need types)
var i int
var c, python, java bool // list of declarations
// declaration with initialization (with optional types)
var i, j int = 1, 2
// declaration with initialization (without types, they are inferred from the initializers)
var i, j = 1, 2
Special Short Form (inside functions)
func main() {
k := 3
c, python, java := true, false, "no!"
...
}
Constants
const Pi = 3.14
Numeric constants are high-precision values. An untyped constant takes the type needed by its context. So potentially, untyped constants may store integers larger than 64-bit.
Default (uninitialized) values
The zero value is:
0
for numeric types,false
for the boolean type, and""
(the empty string) for strings.
Basic Types
- bool
- string
- Integer:
- int (usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems)
- int8
- int16
- int32
- int64
- uint (usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems)
- uint8
- uint16
- uint32
- uint64
- uintptr (usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems)
- byte // alias for uint8
- rune // alias for int32, represents a Unicode code point
- Float:
- float32
- float64
- Complex:
- complex64
- complex128
Function
// function receives two integer parameters (note type is given after variable name) and returns one integer (given after parameter list and before curly braces / function block)
func add(x int, y int) int {
return x + y
}
// types for parameters with same type need only be given once
func add(x, y int) int {
return x + y
}
// a function can return multiple values
func passAlong(x, y int) (int, int) {
return x, y
}
func main() {
a, b := passAlong(5, 7)
}
// return values may be named and act as variable declarations then
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return x, y
}
Naked Return (Questionable Style)
// return statement without paramets will return named return values
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
For Loop
// for <init stmt>;<cond expr>;<post stmt> {}
for i := 0; i < 10; i++ {
sum += i
}
<init stmt>
:- the init statement is executed before the first iteration
- typically a variable declaration (often in short form
i := 0
) - variable only visible in for loop
- the statement is optional
<cond expr>
:- the condition expression is evaluated before every iteration
- loop will stop once the condition evalues to false
- optional, will the create infinite loop
for { ... }
<post stmt>
:- the post statement is executed at the end of every iteration
- the statement is optional
While Loop
Go does not have a while loop, but a for loop without init and post statement takes the role.
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
}
IF
if x > 5 {
return 7 + x
} else {
return 7 - x
}
with optional short-form variable assignment
Variable (y
below) only accesible in if-statement
if y := 27 * x; y > 55 {
return y
}
Switch
- implicit
break
after each case, no run-through as in other languages - but order still matters, the first (top to bottom) matching case will be executed
- case values can be dynamic expressions and non-integer types
- variables may be declared that are then only accessible within switch statement
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
Switch statement may also be used without condition variable/value. The effect is a condition value of true
compared against the case expression. This yields the same behavior as an if-else chain.
switch {
case x > 5:
...
case x < 2:
...
default:
...
}
defer
Now something special. Using the defer
keyword, a function call can be deferred until after the current (surrounding) function returns. Parameters to deferred functions are evaluated immediately though.
The following code will order a pizza after the day’s work is done.
LDFE (LIFO) order: Deferring a function will push it onto a stack, subsequent deferred functions will be pushed on top. So the first function deferred will be the last one executed - last-in-first-out. Last-deferred-first-executed.
func main() {
defer orderPizza("XL")
getSomeWorkDone()
}
Typical case for the use of defer
are clean up operations.
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
Behavior of deferred statements in 3 rules:
- A deferred function’s arguments are evaluated when the defer statement is evaluated (read: when the function is deferred, NOT when the deferred function is executed).
- Deferred function calls are executed in Last In First Out order after the surrounding function returns.
- Deferred functions may read and assign to the returning function’s named return values. The following function will return
2
.func c() (i int) { defer func() { i++ }() return 1 }
Panic and Recover
Calling panic(panicValue)
inside a function F has the following effect:
- Regular control flow is stopped (execution of function is stopped).
- All deferred functions are executed regularly (LIFO stack).
- Function F returns to its caller and acts like a call to
panic()
there (go back to 1.). If F is the main function, the programm will crash.
Calling recover()
inside a function F has the following effect:
- In panic mode:
- The panic mode is stopped. When F returns, it will not have panicking effect on its caller.
- The call to
recover()
returns thepanicValue
from the call topanic(panicValue)
.
- Not in panic mode:
- The call to
recover()
returnsnil
. It has no other effect.
- The call to
Because recover()
is used to handle an error and its resulting panic mode, a call to recover()
only makes sense in deferred functions. Otherwise its execution will be skipped due to panic mode.
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered: ", r)
}
}()
dontPanic()
}
func dontPanic() {
panic("don't panic!!")
}
Pointers
var i int = 742
var p *int = &i
fmt.Println(*p)
*p = 42 // i = 42
Structs
type Size struct {
W int
H int
}
var (
s Size = Size{1280, 1024}
s1 = Size{} // 0,0
s2 = Size{W: 100} // 100,0
pS = &Size{1280, 1024} // pointer to Size{1280, 1024}
)
s.W = 1920
fmt.Println(s)
With pointers
type Size struct {
W int
H int
}
var s Size = Size{1280, 1024}
var p *Size = &s
(*p).W = 2560
p.H = 1440 // == (*p).W !!! implicit dereference before field accessed
Arrays
var a [3]int // array size is part of type -> no resize
a[0] = 0
a[1] = 1
a[2] = 2
var b [5]int = [5]int{0,1,2,3,4}
Slices
c := b[2:5] // slice range is [inclusive:exclusive]
fmt.Println(c) // [2 3 4] not 5
c[0] = 42 // a slice is actually a reference, not a copy!
fmt.Println(b) // b == [0 1 42 3 4]
// this
x1 := [3]int{0,1,2}
x1SliceRef := x1[0:3]
// is equivalent to this "slice literal"
x1SliceRef := []int{0,1,2}
// slice defaults
y := [3]int{0,1,2}
// equivalent slices
y[0:3]
y[0:]
y[:3]
y[:]
Unit Testing
- Create a file ending in
_test.go
in the same package as the code under test. import "testing"
- implement a function
func TestXxx (t *testing.T) {}
whereXxx
is the name of the test case
More
More to come. Maybe. Stay tuned.