The most important Go CLIs:
Command | Description |
---|---|
go build |
Compile packages and dependencies |
go fmt |
Format package sources |
go install |
Build and produce an executable binary of the current package, and install it to the path specified by either GOBIN or GOPATH |
go run |
Compile and run program |
go test |
Test packages |
In Go, a name is exported if it begins with a capital letter
The syntax :=
for short variable declaration can only be used within a function.
Basic types:
bool
string
// The int, uint, and uintptr types are usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems. When you need an integer value you should use int unless you have a specific reason to use a sized or unsigned integer type.
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
// alias for uint8
byte
// alias for int32
// represents a Unicode code point
rune
float32 float64
complex64 complex128
if
and switch
are the 2 ways to check conditions.
if
and switch
accepts an optional initialization statement. This is commonly used to set up a local variable.
You can label a loop and break
the surrounding loop using break LoopLabel
(details here)
// The local variable 'err' is setup in the 'if's initialization statement.
if err := file.Chmod(0664); err != nul {
return err
}
switch {
case 0:
// ...
case 1, 2: // Cases can be comma-separated
// ...
}
There is no do
or while
, only for
loops.
// Like a C for
for i := 0; i < 10; i++ {
// ...
}
// Like a C while
i := 0
for i < 10 {
// ...
i++
}
// Like a C for(;;)
for {
// Infinite loop...
}
// To loop over an array, slice, string, map, channel, use 'range'
for key, value := range r {
// ...
}
// Same as above, but used when only the keys or indices of the range are needed
for key := range r {
// ...
}
// Same as above, but used when only the values of the range are needed
// The '_' is called a blank identifier.
for _, value := range r {
// ...
}
Functions can return multiple values. We can use this to assign multiple variables at once, for example.
func returnMultiple() (int, int) {
return 1, 2
}
var a, b := returnMultiple()
Functions' return results can be named.
The keyword defer
can be used to defer function execution. Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order.
This is an effective way to deal with situations such as resources that must be released regardless of which path a function takes to return.
There are 2 allocation primitives: new
and make
.
new
allocates memory but does not initialize, but zeroes it. new(T)
allocates zeroed storage for a new item of type T
and returns its addresss, a value of type *T
(a pointer to a newly allocated zero value of type T
).
make
only creates slices, maps, and channels. make(T, args)
returns an initialized (i.e., not zeroed) value of type T
(not *T
). make
does not return a pointer.
The &
operator generates a pointer.
The *
operator denotes a pointer's underlying value.
i, j := 1, 100
p := &i // 'p' is a pointer that holds the memory address of 'i'
fmt.Println(*p) // Will print the underlying value that pointer 'p' points to (i.e. 1)
fmt.Println(p) // Will print the memory address held by pointer 'p'
*p = 2 // Set the value of 'i' to 2 through the pointer 'p'
*p = *p + 1 // Another operation. Printing *p should now give the value 3
p = &j // 'p' now points to 'j'
Here are pointers' most common use cases:
- Passing pointers to functions allows the functions to mutate the pointers' underlying data.
- Pointers help distinguish between a zero value and an unset value.
Pointers are NOT a silver bullet for performance optimization. Using pointers have its own costs.
Go: Are pointers a performance optimization? by Kale Blankenship
A struct
is a collection of fields.
type Example struct {
X int
Y int
}
e := Example{1, 2}
p := &e // Create pointer 'p' that points to the struct 'e'
fmt.Println(v.X) // Will print 1
fmt.Println(p.X) // Will print 1. Note that we don't need to do (*p).X
Arrays:
// Declare 'a' as an array of 10 integers
// Note that you can't do a := [10]int
// Arrays can't be resized
var a [10]int
// To use the ':=' syntax, you need to define the array's variables as well
// Any variable not defined inside the "{}" will default to its zero value e.g. 0, false, "", etc.
b := [3]int{1, 2, 3} // [1 2 3]
b := [3]int{} // [0 0 0]
Slices are dynamically-sized, flexible views into the elements of arrays.
Slices do not store any data.
Changes in the elements of slices also modifies the corresponding elements of their underlying arrays.
A slice's length is the number of elements it contains. This can be obtained with len(s)
. A slice's length can be extended by re-slicing it (provided the new length does not exceed its capacity).
A slice's capacity is the number of elements in the underlying array. This can be obtained with cap(s)
.
The make
function can be used to create slices, and in turn, dynamically-sized arrays.
a := [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]
var s []int = a[1:3] // Create the slice [2 3]
s2 := a[1:3] // Create the slice [2 3]
s3 := a[:] // Create the slice [1 2 3 4 5]
// Create the slice [1 2 3] without any base array
// Essentially this creates an array, and then builds a slice that references it
s3 := []int{1, 2, 3}
// Create a zeroed array and returns a slice that refers to that array
s4 := make([]int, 5) // len(s4) = 5
s5 := make([]int, 5, 6) // len(s5) = 5; cap(s5) = 6
Working with slices:
var s []int
s = append(s, 1) // [1]
range
iterates over a slice or map.
for i, v := range []int{1, 2, 3} {
fmt.Println(i, v) // Will print "0 1", "1 2", "2 3"
}
for i, _ := range []int{1, 2, 3} {
// Iterate through the slice, not caring about the slice's values
}
for i := range []int{1, 2, 3} {
// Iterate through the slice, not caring about the slice's values (shorter way)
}
for _, v := range []int{1, 2, 3} {
// Iterate through the slice, not caring about the index
}
Maps hold key-value pairs.
A nil
map has no keys, and additional keys can't be added to nil
maps.
The make
function returns a map of the given type.
type Person struct {
FirstName, LastName string
}
// Declare map 'n'
var n map[string]Person
// Initialize map 'm' and add a value to it
m := make(map[string]Person)
m["a"] = Person{
"John", "Smith",
}
// Declare map literal 'o'
var o = map[string]Person{
"a": {"John", "Smith"},
}
Working with maps:
m := make(map[string]int)
// Insert / update key 'a' of map 'm'
m["a"] = 1
// Retrieve key 'a' of map 'm'
m["a"]
// Delete key 'a' of map 'm'
delete(m, "a")
// Test that key 'a' is present
// If key 'a' is in 'm', 'ok' will be 'true', else 'ok' will be 'false'
// If key 'a' is not in 'm', 'elem' will be the zero value for the element type of map 'm'
elem, ok := m["a"]
Go does not have classes. However, methods can be defined on types.
A method is a regular function with a special receiver argument.
type Pair struct {
A, B int
}
// Declare method Sum() with receiver of type 'Pair' named 'p'
func (p Pair) Sum() int {
return p.A + p.B
}
p := Pair{1, 2}
p.Sum() // 3
Methods can be declared on non-struct types.
type MyInt int
func (i MyInt) Abs() int {
if i < 0 {
return int(-i)
}
return int(i)
}
f := MyInt(-1)
f.Abs() // 1
Methods can be declared with pointer receivers. Methods with pointer receivers can modify the value to which the receiver points to.
By using pointer receivers, methods can modify the values that their receivers point to. This will also avoid copying values on each method call.
All methods on a given type should either have value or pointer receivers, but not a mixture of both.
type Pair struct {
A, B int
}
func (p *Pair) IncreaseBy(n int) {
p.A = p.A + n
p.B = p.B + n
// If the receiver is not a pointer receiver (i.e., if the '*' is removed from '(p *Pair)'),
// the underlying p.A and p.B will never be modified. The operation p.A + n and p.B + n will
// create a copy of p.A and p.B and operate on these copies instead.
// Printing p.A and p.B within this method will thus give the correct modified values, but
// printing p.A and p.B outside this method will give the original values.
// Methods with value receivers can take either a pointer or a value as the receiver when
// they are called, and vice versa.
// Functions, however, are more strict. Functions with a pointer argument must take a
// pointer when they are called, for example.
}
p := Pair{1, 2}
p.IncreaseBy(10) // 'p' is now {11, 12}
An interface type is a set of method signatures.
A type implements an interface by implementing its methods. There is no explicit declaration of intent, or "implements" keyword.
An interface value is a tuple of a value and a concrete type: (value, type)
.
type Abser interface {
Abs() int
}
type MyInt int
func (i MyInt) Abs() int {
// Gracefully handle being called with a 'nil' receiver
if i == nil {
fmt.Println("<nil>")
return
}
if (i < 0) {
return int(-i)
}
return int(i)
}
a := MyInt(-1) // 'a' is a 'MyInt', which implements 'Abser'
The interface type that specifies zero methods is known as the empty interface. An empty interface may hold values of any type.
Empty interfaces are used by code that handles values of unknown type.
var i interface{}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
describe(i) // (<nil>, <nil>)
A type assertion provides access to an interface value's underlying concrete value.
// The interface value 'i' holds the concrete type 'T' and assigns the underlying 'T'
// value to the variable 't'.
// If 'i' does not hold a 'T', this statement will trigger a panic.
t := i.(T)
// This can be used to gracefully test whether an interface value holds a specific
// type. The 'ok' variable will be true with 'i' holds a 'T'
t, ok := i.(T)
A type switch is a construct that permits several type assertions in series.
A type switch is like a regular switch statement, but the cases specify types rather than values, and those values are compared against the type of the value held by the given interface value.
switch v := i.(type) {
case T:
// here v has type T
case S:
// here v has type S
default:
// no match; here v has the same type as i
}
A Stringer
is a type that can describe itself as a string.
type Stringer interface {
String() string
}
Functions often return an error
value, and calling code should handle errors by testing whether the error equals nil
.
type error interface {
Error() string
}
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
The io
package specifies the io.Reader
interface, which represents the read end of a stream of data.
Implementations of this interface can be found in the Go standard library here.
A common pattern is an io.Reader
that wraps another io.Reader
, modifying the stream in some way.
// Read populates the given byte slice with data and returns the number of bytes
// populated and an error value. It returns an 'io.EOF' error when the stream
// ends.
func (T) Read(b []byte) (n int, err error)
/*
* An example reader:
*/
import (
"fmt"
"io"
"strings"
}
func main() {
r := strings.NewReader("Hello there!")
// The reader will consume its output 8 bytes at a time
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b =%v\n", n, err, b)
fmt.Printf("b[:n] = %q\n, b[:n]) // Will print "Hello, t", "here!", and ""
if err == io.EOF {
break
}
}
}
Package image defines the Image
interface
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
A goroutine is a lightweight thread managed by the Go runtime.
Goroutines run in the same address space, so access to shared memory must be synchronized. The sync
package provides useful primitives, although you won't need them much in Go as there are other primitives.
// Start a new goroutine running f(x, y, z)
// The evaluation of 'f', 'x', 'y', and 'z' happens in the current goroutine
// and the execution of 'f' happens in the new goroutine.
go f(x, y, z)
Channels are typed conduits through which you can send and receive values with the channel operator <-
.
Channels must be created before use e.g. make
.
By default, sending and receiving blocks until the other side is ready.
ch := make(chan int)
// Send v to channel 'ch'
ch <- v
// Receive from 'ch' and assign valuye to 'v'
v := <- ch
Example channel usage:
// A function that sums a slice of integers and sends the result
// to a channel
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
ch := make(chan int)
// Split slice 's' in half and distribute work between 2
// goroutines
go sum(s[len(s)/2:], c)
go sum(s[:len(s)/2], c)
// Once both goroutines have completed their computations,
// calculate the final result.
x, y := <-ch, <-ch
fmt.Println(x, y, x+y) // Will print "17 -5 12"
}
Channels can be buffered. Sends to a buffered channel block only when the buffer is full. Receives from a buffered channel block only when the buffer is empty.
// Create a channel with buffer length 100
ch := make(chan int, 100)
A sender can close a channel with close(ch)
to indicate that no more values will be sent. Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression.
When used with a loop, the expression for i := range ch
receives values from the channel 'ch' repeatedly until 'ch' is closed.
Only the sender should close a channel. A receiver should never close a channel.
Channels aren't like files and normally don't need to be closed. It's only necessary to close channels when the receiver must be told there are no more values incoming (e.g. terminating a range
loop).
// 'ok' is 'false' if there are no more values to receive
// and the channel is closed
v, ok := <- ch
The select
statement lets a goroutine wait on multiple communication oeprations.
A select
blocks until one of its cases can run, then it executes that case. If multiple cases are ready, a random case will be chosen.
The default
case in a select
is run if no other case is ready.
select{
case i := <-ch:
// Use i
default:
// Receiving from 'ch' would block
}