Why Go?
Go is a statically typed, compiled programming language. It was designed at Google in 2007 to improve programming productivity in an era of multicore, networked machines and large codebases. The designers wanted to address criticism of other languages in use at Google, but keep their useful characteristics.
Go is syntactically similar to C, but with memory safety, garbage collection, structural typing, and CSP-style concurrency. More of the features:
- Minimalism: easy to learn, read, write and maintain
- Compatibility: succinct and strict compatibility guarantees
- Performance: compiled language similar to C, optimized standard libraries, built-in concurrency primitives (lower latency and smaller overhead)
- Development: automatic documentation, built-in frameworks for testing and profiling, clear code separation, race condition detector, etc.
- OOP without classes, inheritance, constructors, generics, and others
This tutorial onwards is almost completely based on A Tour of Go and How to Write Go Code.
Go Basics
Packages and Functions
Every Go program is made up of packages. Programs start running in package main
. The following program is using the packages with import paths "fmt"
and "math/rand"
.
The imports are grouped into a parenthesized statement. Writing multiple import statements is also acceptable but not considered a good style.
1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}
In Go, a name is exported if it begins with a capital letter. For example, Pizza
is an exported name, as is Pi
, which is exported from the math
package. In contrast, pizza
and pi
do not start with a capital letter, so they are not exported.
When importing a package, you can refer only to its exported names. Any “unexported” names are not accessible from outside the package.
1
fmt.Println(math.pi) // error: cannot refer to unexported name math.pi
A function can take zero or more arguments. In this example, add
takes two parameters of type int
. Notice that the type comes after the variable name. This is believed to be easier to understand compared to C syntax, especially when things get complicated. (See Go’s declaration syntax for more explanations.)
1
2
3
4
5
6
7
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
When two or more consecutive named function parameters share a type, you can omit the type from all but the last. Also, a function can return any number of results. The following swap
function takes two strings and returns two strings.
1
2
3
func swap(x, y string) (string, string) {
return y, x
}
Go’s return values may be named. If so, they are treated as variables defined at the top of the function. These names should be used to document the meaning of the return values.
A return
statement without arguments returns the named return values. This is known as a “naked” return. Naked return statements should be used only in short functions, otherwise they can harm readability in longer functions.
1
2
3
4
5
func split(sum int) (x, y int) {
x = sum * 4 / 9 // int division
y = sum - x
return
}
Variables and Types
The var
statement declares a list of variables; as in function argument lists, the type is placed at last. A var
statement can be at package or function level.
A var declaration can include initializers, one per variable. If an initializer is present, the type can be omitted; the variable will take the type of the initializer. Variables declared without an explicit initial value are given their zero value, which is 0
for numeric types, false
for boolean, and empty string for strings.
1
2
3
4
5
6
7
var i, j int = 1, 2
var b bool
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, b, c, python, java) // 1 2 false true false no!
}
Inside a function, the :=
short assignment statement can be used in place of a var
declaration with implicit type. Outside a function, every statement begins with a keyword (var
, func
, and so on) and so the :=
construct is not available.
1
2
3
4
5
func main() {
var i, j int = 1, 2
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}
Go’s basic types are
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32, represents a Unicode code point
float32 float64
complex64 complex128
The following example shows variables of several types, and also that variable declarations may be “factored” into blocks, as with import statements. When you need an integer value you should use int
unless you have a specific reason to use a sized or unsigned integer type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"math/cmplx"
)
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe) // Type: bool Value: false
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt) // Type: uint64 Value: 18446744073709551615
fmt.Printf("Type: %T Value: %v\n", z, z) // Type: complex128 Value: (2+3i)
}
In fmt
package, the format string for printing is similar to C but greatly simplified. For example, %v
presents the value in a default format, and %T
presents a Go-syntax representation of the type of the value. See Package fmt for more information.
Unlike in C, in Go assignment between items of different type requires an explicit conversion. The expression T(v)
converts the value v
to the type T
. For example:
1
2
3
i := 42
f := float64(i)
u := uint(f)
Constants are declared like variables, but with the const
keyword. Constants cannot be declared using the :=
syntax. For example, const Pi = 3.14
.
Pointers: A pointer holds the memory address of a value. The type *T
is a pointer to a T
value. Its zero value is nil
. The &
operator generates a pointer to its operand. The *
operator denotes the pointer’s underlying value. Unlike C, Go has no pointer arithmetic.
1
2
3
4
5
6
7
func main() {
i := 42
p := &i // point to i
fmt.Println(*p) // read i through the pointer: 42
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i: 21
}
Flow Control Statements
Loops: Go has only one looping construct, the for
loop. The basic for
loop, similar to C, has three components separated by semicolons:
- the init statement: executed before the first iteration
- the condition expression: evaluated before every iteration
- the post statement: executed at the end of every iteration
The init statement will often be a short variable declaration, and the variables declared there are visible only in the scope of the for
statement.
Note: Unlike other languages like C, Java, or JavaScript, there are no parentheses surrounding the three components of the for
statement and the braces { }
are always required.
1
2
3
4
5
6
7
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum) // 45
}
The init and post statements are optional. If you omit the loop condition it loops forever. You still use for
for a while loop in C. Just simply drop the semicolons.
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
sum := 1
for ; sum < 10; sum += 1 {
sum *= 2
fmt.Println(sum) // 2 6 14
}
for sum < 64 {
sum *= 2
fmt.Println(sum) // 30 60 120
}
for { // infinite loop
}
}
Conditional statements: Go’s if
statements are like its for
loops; the expression need not be surrounded by parentheses ( )
but the braces { }
are required.
Like for
, the if
statement can start with a short statement to execute before the condition. Variables declared by the statement are only in scope until the end of the if
, including any of the else
blocks followed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%v >= %v\n", v, lim)
}
// can't use v here, though
return lim
}
func main() {
fmt.Println(pow(3, 2, 10), pow(3, 3, 20))
// First line: 27 >= 20
// Second line: 9 20
}
A switch
statement is a shorter way to write a sequence of if - else
statements. It runs the first case whose value is equal to the condition expression.
Go’s switch is like the one in C, C++, Java, etc., except that Go only runs the selected case, not all the cases that follow. In effect, the break
statement that is needed at the end of each case in those languages is provided automatically in Go. Switch cases evaluate cases from top to bottom, stopping when a case succeeds.
Another important difference is that Go’s switch cases need not be constants, and the values involved need not be integers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
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 without a condition is the same as switch true
. This construct can be a clean way to write long if-then-else chains.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
Defer statement: A defer statement defers the execution of a function until the surrounding function returns. The deferred call’s arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order. To learn more about defer statements read this blog post.
1
2
3
4
5
6
7
func main() {
fmt.Println("counting")
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
fmt.Println("done") // counting done 4 3 2 1 0
}
More Types: Structs, Arrays, Slices, Maps
Structs: A struct
is a collection of fields. Struct fields are accessed using a dot. Struct fields can also be accessed through a struct pointer. To access the field X
of a struct when we have the struct pointer p
we could write (*p).X
. However, that notation is cumbersome, so the language allows just p.X
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &v3 // has type *Vertex
)
func main() {
fmt.Println(v1) // {1 2}
fmt.Println(v2.X, v2.Y) // 1 0
p.X = 4
fmt.Println(p) // &{4 0}
}
Arrays: The type [n]T
is an array of n
values of type T
. An array’s length is part of its type, so arrays cannot be resized. Although an array has a fixed size, a slice is a dynamically-sized, flexible view into the elements of an array. In practice, slices are much more common than arrays.
The type []T
is a slice with elements of type T
. A slice is formed by specifying two indices, a low and high bound, separated by a colon: a[low : high]
. This selects a half-open range which includes the first element, but excludes the last one. When slicing, you may omit the high or low bounds to use their defaults instead. The default is zero for the low bound and the length of the slice for the high bound.
This is an array literal: [3]bool{true, true, false}
. And this creates the same array as above, then builds a slice that references it: []bool{true, true, false}
.
1
2
3
4
5
6
7
8
9
10
11
func main() {
var a [2]string
a[0] = "Hello"
fmt.Println(a) // [Hello ]
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes) // [2 3 5 7 11 13]
var s []int = primes[:4]
fmt.Println(s) // [2 3 5 7]
}
Changing the elements of a slice modifies the corresponding elements of its underlying array. Other slices that share the same underlying array will see those changes.
A slice has both a length and a capacity. The length of a slice is the number of elements it contains. The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice. The length and capacity of a slice s
can be obtained using the expressions len(s)
and cap(s)
.
The zero value of a slice is nil
. A nil slice has a length and capacity of 0 and has no underlying array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
numbers := [6]int{2, 3, 5, 7, 11, 13}
s := numbers[:2]
printSlice(s) // len=2 cap=6 [2 3]
s = s[:4]
printSlice(s) // len=4 cap=6 [2 3 5 7]
s = s[2:]
printSlice(s) // len=2 cap=4 [5 7]
s = s[:3]
printSlice(s) // len=3 cap=4 [5 7 11]
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
Slices can be created with the built-in make
function; this is how you create dynamically-sized arrays. In the following example, the make
function allocates a zeroed array and returns a slice that refers to that array. To specify a capacity, pass a third argument to make
.
1
2
a := make([]int, 5) // len(a)=5, cap(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
Go provides a built-in append
function to append new elements to a slice. Example usage:
1
2
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
The range
form of the for
loop iterates over a slice or map. When ranging over a slice, two values are returned for each iteration. The first is the index, and the second is a copy of the element at that index. You can skip the index or value by assigning to _
, or if you only want the index, you can omit the second variable.
1
2
3
4
5
6
7
8
9
func main() {
pow := make([]int, 5)
for idx := range pow {
pow[idx] = 1 << i
}
for idx, value := range pow {
fmt.Printf("%d %d\n", idx, value) // 0 1; 1 2; 2 4; 3 8; 4 16
}
}
A map maps keys to values. The zero value of a map is nil
. A nil
map has no keys, nor can keys be added. The make
function returns a map of the given type, initialized and ready for use.
Map operations:
- Insert or update an element in map
m
:m[key] = elem
- Retrieve an element:
elem = m[key]
- Test that a key is present with a two-value assignment:
elem, ok = m[key]
.ok
is true ifkey
is inm
and false otherwise. Whenok
is false,elem
is the zero value of the element type. - Delete an element:
delete(m, key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Vertex struct {
Lat, Long float64
}
func main() {
l := make(map[string]int)
l["what"] = 2
fmt.Println(l) // map[what:2]
var m = map[string]Vertex{
"Bell Labs": {40.68, -74.39}, // Vertex type omitted in literals
"Google": {37.42, -122.08},
}
fmt.Println(m) // map[Bell Labs:{40.68 -74.39} Google:{37.42 -122.08}]
}
Functional Programming
Functions are values too. They can be passed around just like other values. Function values may be used as function arguments and return values.
1
2
3
4
5
6
7
8
9
10
11
12
13
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
Go functions may be closures. A closure is a function value that references variables from outside its body. The function may access and assign to the referenced variables; in this sense the function is “bound” to the variables. In the following example, the adder
function returns a closure. Each closure is bound to its own sum
variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(pos(i), neg(-2*i))
}
}
Methods and Interfaces
Methods
Go does not have classes. However, you can define methods on types. A method is a function with a special receiver argument. The receiver appears in its own argument list between the func
keyword and the method name. In the following example, the Abs
method has a receiver of type Vertex
named v
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs()) // 5
v.Scale(10)
fmt.Println(v.Abs()) // 50
}
You can also declare methods with pointer receivers. Methods with pointer receivers can modify the value to which the receiver points. Note that methods with pointer receivers take either a value or a pointer as the receiver when they are called. In the example above, even though v
is a value and not a pointer, Go interprets the statement v.Scale(5)
as (&v).Scale(5)
since the Scale
method has a pointer receiver.
You can only declare a method with a receiver whose type is defined in the same package as the method. That means you cannot declare a method with a receiver whose type is defined in another package, including built-in types such as int
.
There are two reasons to use a pointer receiver. The first is so that the method can modify the value that its receiver points to. The second is to avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example.
Interfaces
An interface type is defined as a set of method signatures. A value of interface type can hold any value that implements those methods. A type implements an interface by implementing its methods. There is no explicit “implements” keyword.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat implements Abser
a = &v // a *Vertex implements Abser
// In the following line, v is a Vertex (not *Vertex)
// and does NOT implement Abser.
a = v
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
Under the hood, interface values can be thought of as a tuple of a value and a concrete type: (value, type)
. An interface value holds a value of a specific underlying concrete type. Calling a method on an interface value executes the method of the same name on its underlying type.
If the concrete value inside the interface itself is nil, the method will be called with a nil receiver. A nil interface value holds neither value nor concrete type. Calling a method on a nil interface is a run-time error because there is no type inside the interface tuple to indicate which concrete method to call.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
describe(i) // (<nil>, <nil>)
// i.M() // runtime error
var t *T
i = t
describe(i) // (<nil>, *main.T)
i.M() // <nil>
i = &T{"hello"}
describe(i) // (&{hello}, *main.T)
i.M() // hello
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
The interface type that specifies zero methods is known as the empty interface: interface{}
. An empty interface may hold values of any type. Empty interfaces are used by code that handles values of unknown type.
1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var i interface{}
describe(i) // (<nil>, <nil>)
i = 42
describe(i) // (42, int)
i = "hello"
describe(i) // (hello, string)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
A type assertion provides access to an interface value’s underlying concrete value.
The statement t := i.(T)
asserts that 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
, the statement will trigger a panic.
To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded, which resembles t, ok := i.(T)
. When ok
is false, t
is the zero value of the type T, and no panic occurs.
1
2
3
4
5
6
7
8
9
10
11
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s) // hello
s, ok := i.(string)
fmt.Println(s, ok) // hello true
f, ok := i.(float64)
fmt.Println(f, ok) // 0 false
f = i.(float64) // panic: interface conversion
fmt.Println(f)
}
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 in a type switch specify types (not values), and those values are compared against the type of the value held by the given interface value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21) // Twice 21 is 42
do("hello") // "hello" is 5 bytes long
do(true) // I don't know about type bool!
}
Common Interfaces
One of the most ubiquitous interfaces is Stringer
defined by the fmt
package.
1
2
3
type Stringer interface {
String() string
}
A Stringer
is a type that can describe itself as a string. The fmt
package (and many others) look for this interface to print values.
1
2
3
4
5
6
7
8
9
10
11
12
13
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
fmt.Println(a) // Arthur Dent (42 years)
}
Errors: Go programs express error state with error
values. The error
type is a built-in interface similar to fmt.Stringer
. The fmt
package also looks for the error
interface when printing error messages.
1
2
3
type error interface {
Error() string
}
Functions often return an error
value, and calling code should handle errors by testing whether the error equals nil
. A nil error
denotes success; a non-nil error
denotes failure.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import (
"fmt"
"math"
)
type ErrNegativeSqrt struct {
X float64
}
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative number: %v", e.X)
}
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, ErrNegativeSqrt(x)
}
return math.Sqrt(x), nil
}
func main() {
x := -2.0
root, err := Sqrt(x)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("sqrt(%v) = %v", x, root)
}
}
Readers: The io
package specifies the io.Reader
interface, which represents the read end of a stream of data. The Go standard library contains many implementations of these interfaces, including files, network connections, compressors, ciphers, and others. The io.Reader
interface has a Read
method:
1
func (T) Read(b []byte) (n int, err error)
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. The example code creates a strings.Reader
and consumes its output 8 bytes at a time.
A common pattern is an io.Reader that wraps another io.Reader
, modifying the stream in some way. For example, the gzip.NewReader function takes an io.Reader
(a stream of compressed data) and returns a *gzip.Reader
that also implements io.Reader
(a stream of the decompressed data).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Hello, Reader!")
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])
if err == io.EOF {
break
}
}
/*
Output:
n = 8 err = <nil> b = [72 101 108 108 111 44 32 82]
b[:n] = "Hello, R"
n = 6 err = <nil> b = [101 97 100 101 114 33 32 82]
b[:n] = "eader!"
n = 0 err = EOF b = [101 97 100 101 114 33 32 82]
b[:n] = ""
*/
}
Concurrency
Goroutines and Channels
Goroutines: A goroutine is a lightweight thread managed by the Go runtime. Statement go f(x, y, z)
starts 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.
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. For example, channels.
Channels are a typed conduit through which you can send and receive values with the channel operator <-
. The data flows in the direction of the arrow.
1
2
3
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
Like maps and slices, channels must be created before use: ch := make(chan int)
. By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.
The following code sums the numbers in a slice, distributing the work between two goroutines. Once both goroutines have completed their computation, it calculates the final result.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y) // -5 17 12 or 17 -5 12
}
Channels can be buffered. Provide the buffer length as the second argument to make
to initialize a buffered channel: ch := make(chan int, 100)
. Sends to a buffered channel block only when the buffer is full. Receives block when the buffer is empty.
A sender can close
a channel 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: v, ok := <-ch
, ok
is false
if there are no more values to receive and the channel is closed. The loop for i := range ch
receives values from the channel repeatedly until it is closed.
Note: Only the sender should close a channel, never the receiver. Sending on a closed channel will cause a panic.
Another note: Channels aren’t like files; you don’t usually need to close them. Closing is only necessary when the receiver must be told there are no more values coming, such as to terminate a range
loop.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i) // 0 1 1 2 3 5 8 13 21 34
}
}
The select
statement lets a goroutine wait on multiple communication operations. A select
blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.
The default
case in a select
is run if no other case is ready. Use a default
case to try a send or receive without blocking:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import (
"fmt"
"time"
)
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
default:
fmt.Print("waiting...")
time.Sleep(1000)
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 5; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
/*
waiting... waiting... 0
waiting... 1
waiting... 1
waiting... 2
waiting... 3
quit
*/
}
Package sync: Mutexes, WaitGroups
We’ve seen how channels are great for communication among goroutines. But what if we don’t need communication? What if we just want to make sure only one goroutine can access a variable at a time to avoid conflicts?
This concept is called mutual exclusion, and the conventional name for the data structure that provides it is mutex. Go’s standard library provides mutual exclusion with sync.Mutex
and its two methods: Lock
and Unlock
.
As in the following example, we can define a block of code to be executed in mutual exclusion by surrounding it with a call to Lock
and Unlock
as shown on the Inc
method. We can also use defer
to ensure the mutex will be unlocked as in the Value
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc increments the counter for the given key.
// Without mutex this will lead to 'fatal error: concurrent map writes'
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
c.v[key]++
c.mux.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
defer c.mux.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey")) // 1000
}
Note that all the goroutines will halt when the main()
function returns. (No defer
statements will be run in this case if any.) So for example, the following program will be likely to print nothing.
1
2
3
4
5
6
7
8
9
func say(s string) {
fmt.Println(s)
}
func main() {
for _, s := range []string{"hello", "world"} {
go say(s)
}
}
One way to wait for all goroutines to finish is to use sync.WaitGroup
. In this example, we launched several goroutines and incremented the WaitGroup counter for each. Each goroutine notifies the WaitGroup that it’s done on return. wg.Wait()
will block until the WaitGroup counter goes back to 0, i.e., when all the workers have notified they’re done. Note that a WaitGroup must be passed to functions by pointer.
In theory, this can also be done using channels. Each goroutine sends a done
signal to a channel on return, and the goroutine caller blocks to consume all the signals before it returns.
1
2
3
4
5
6
7
8
9
10
11
12
13
func say(s string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(s)
}
func main() {
var wg sync.WaitGroup
for _, s := range []string{"hello", "world"} {
wg.Add(1)
go say(s, &wg)
}
wg.Wait()
}
Code Development
Package Organization
Go programs are organized into packages. A package is a collection of source files in the same directory that are compiled together. A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository. A file named go.mod
there declares the module path: the import path prefix for all packages within the module.
Note that you don’t need to publish your code to a remote repository before you can build it. A module can be defined locally without belonging to a repository. However, it’s a good habit to organize your code as if you will publish it someday.
To compile and run a simple program, first choose a module path (we’ll use example.com/user/hello
) and create a go.mod
file that declares it:
1
2
3
4
5
6
7
8
$ mkdir hello
$ cd hello
$ go mod init example.com/user/hello
go: creating new go.mod: module example.com/user/hello
$ cat go.mod
module example.com/user/hello
go 1.14
In this directory, let’s write a morestrings
package and use it from a hello
program. First, create a directory for the package named hello/morestrings
, and then a file named reverse.go
in that directory with the following contents:
1
2
3
4
5
6
7
8
9
10
package morestrings
// ReverseRunes returns its argument string reversed rune-wise left to right.
func ReverseRunes(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
Then we create a file in hello
named hello.go
.
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
"example.com/user/hello/morestrings" // local package
"github.com/google/go-cmp/cmp" // remote package
)
func main() {
fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
fmt.Println(cmp.Diff("Hello World", "Hello Go"))
}
Then we can do either go run
, go build
or go install
in the hello
directory.
- If we do
go run .
, or more specificallygo run hello.go
, the main package will be compiled and directly run on the compiled binary. The binary is not stored anywhere. - If we do
go build
/go build .
, or more specificallygo build example.com/user/hello
, then an executable namedhello
will be built in the current directory, and one can run the binary by./hello
. - If we do
go install
/go install .
, orgo install example.com/user/hello
, then the executable will not appear in the current directory. Rather, it will be installed in$HOME/go/bin/hello
and can be invoked directly viahello
. The install directory is controlled by theGOPATH
andGOBIN
environment variables.
Import path can also involve remote repositories. When you run commands like go install
, go build
, or go run
, the go
command will automatically download the remote module and record its version in your go.mod
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ go build
go: finding module for package github.com/google/go-cmp/cmp
go: downloading github.com/google/go-cmp v0.4.1
go: found github.com/google/go-cmp/cmp in github.com/google/go-cmp v0.4.1
$ ./hello
Hello, Go!
string(
- "Hello World",
+ "Hello Go",
)
$ cat go.mod
module example.com/user/hello
go 1.14
require github.com/google/go-cmp v0.4.1
Testing and Benchmarking
Go has a lightweight test framework composed of the go test
command and the testing
package.
You write a test by creating a file with a name ending in _test.go
that contains functions that take the form of func TestXxx(*testing.T)
. The test framework runs each such function; if the function calls a failure function such as t.Error
or t.Fail
, the test is considered to have failed.
Let’s add a test to the morestrings
package by creating the file hello/morestrings/reverse_test.go
containing the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package morestrings
import "testing"
func TestReverseRunes(t *testing.T) {
cases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{"Hello, 世界", "界世 ,olleH"},
{"", ""},
}
for _, c := range cases {
got := ReverseRunes(c.in)
if got != c.want {
t.Errorf("ReverseRunes(%q) == %q, want %q", c.in, got, c.want)
}
}
}
Then run the test with go test
in the hello/morestrings
directory:
1
2
3
4
5
$ pwd
[...]/hello/morestrings
$ go test
PASS
ok example.com/user/hello/morestrings 0.007s
Functions of the form func BenchmarkXxx(*testing.B)
are considered benchmarks. By default, no benchmarks are run when the go test
command is executed. It is only run when a -bench flag is provided. Benchmarks are run sequentially. For example, let’s add a benchmark test in the reverse_test.go
file.
1
2
3
4
5
func BenchmarkReverseRunes(b *testing.B) {
for i := 0; i < b.N; i++ {
ReverseRunes("Hello, world")
}
}
Then we run go test -bench=.
. The -bench flag taken by the go test
command accepts a regular expression, and only the benchmarks that match the expression will be run.
The benchmark function must run the target code b.N times. During benchmark execution, b.N is adjusted until the benchmark function lasts long enough to be timed reliably. In the following example, the loop ran 5794428 times at a speed of 203 ns per loop. See more examples at Package testing.
1
2
3
4
5
6
7
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: example.com/user/hello/morestrings
BenchmarkReverseRunes-4 5794428 203 ns/op
PASS
ok example.com/user/hello/morestrings 2.377s