Go 编程之旅
Welcome
Offline preparation:
go install golang.org/x/website/tour@latest
tour
Hotkeys
Ctrl + Enter
to format codeShift + Enter
to run codePage Up
&Page Down
to switch between pages.
Packages, variables, and functions
Packages & Imports
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}
All exported names should start with a capital character:
fmt.Println(math.Pi)
Functions
func add(x int, y int) int {
return x + y
}
Omit the type from all but the last:
func add(x, y int) int {
return x + y
}
You can return multiple values:
func swap(x, y string) (string, string) {
return y, x
}
And you can name return values:
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // this is called "naked return"
}
Variables
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
Variables with initializers (type declaration can be ommited in this case):
var i, j int = 1, 2
var c, python, java = true, false, "no!" // type ommited
Short variable declarations (only working when inside a function):
func foo() {
c, python, java := true, false, "no!"
foo := func() {} // also work for function decalarations
foo()
}
Basic Types
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
Zero Values:
Variables declared without an explicit initial value are given their zero value:
- 0 for numeric types,
- false for the boolean type, and
- "" (the empty string) for strings.
Type Conversions
i := 42
f := float64(i)
u := uint(f)
Go forbid implicit conversions.
Constants
const Truth = true
Please notice constants cannot be declared using the := syntax.
Flow control statements: for, if, else, switch and defer
For
for i := 0; i < 1000; i++ {
sum += 1
}
// i is not accessible outside the loop
for ; sum < 1000; {
sum += i
}
for sum < 1000 {
sum += i
}
for {
sum += i
}
The basic for loop 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
If & Else
if x < 0 {
// do something
}
if v := math.Pow(x, n); v < lim {
return v
} else {
return v
}
// same as for, v is not accessible outside the brace
Switch
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
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.")
}
- Go only runs the selected case, not all the cases that follow.
- Another important difference is that Go's switch cases need not be constants, and the values involved need not be integers.
- Switch cases evaluate cases from top to bottom, stopping when a case succeeds.
- Switch without a condition is the same as switch true. This construct can be a clean way to write long if-then-else chains.
Defer
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
// will print: 9 8 7 ...
- 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.
More types: structs, slices, and maps
Pointers
- The type *T is a pointer to a T value. Its zero value is nil.
var p *int
- The & operator generates a pointer to its operand.
p = &i
- The * operator denotes the pointer's underlying value.
fmt.Println(*p) // read i through the pointer p
*p = 21 // set i through the pointer p
- Unlike C, Go has no pointer arithmetic.
Structs
type Vertex struct {
X int
Y int
}
// Struct Literals
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &Vertex{1, 2} // has type *Vertex
)
func main() {
v := Vertex{1, 2}
v.X = 4
p := &v
p.X = 1
}
- A struct is a collection of fields.
- Struct fields are accessed using a dot.
- 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 permits us instead to write just p.X, without the explicit dereference.
Arrays
- The type [n]T is an array of n values of type T.
var a [10]int
primes := [6]int{2, 3, 5, 7, 11, 13}
- An array's length is part of its type, so arrays cannot be resized.
Slices
An array has a fixed size. A slice, on the other hand, 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.
var s []int = primes[1:4]
A slice is formed by specifying two indices, a low and high bound, separated by a colon. This selects a half-open range which includes the first element, but excludes the last one.
Slices are like references to arrays. A slice does not store any data, it just describes a section of an underlying array.
A slice literal is like an array literal without the length.
q := []int{2, 3, 5, 7, 11, 13} s := []struct { i int b bool }{ {2, true}, {3, false}, {5, true}, {7, true}, {11, false}, {13, true}, }
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.
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).
func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // len=6 cap=6 [2 3 5 7 11 13] // Slice the slice to give it zero length. s = s[:0] printSlice(s) // len=0 cap=6 [] // Extend its length. s = s[:4] printSlice(s) // len=4 cap=6 [2 3 5 7] // Drop its first two values. s = s[2:] printSlice(s) // len=2 cap=4 [5 7] } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) }
The zero value of a slice is nil. A nil slice has a length and capacity of 0 and has no underlying array.
Slices can be created with the built-in make function; this is how you create dynamically-sized arrays.
a := make([]int, 5) // len(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
Slices can contain any type, including other slices.
board := [][]string{ []string{"_", "_", "_"}, []string{"_", "_", "_"}, []string{"_", "_", "_"}, }
It is common to append new elements to a slice, and so Go provides a built-in append function.
func append(s []T, vs ...T) []T
- The first parameter s of append is a slice of type T, and the rest are T values to append to the slice.
- The resulting value of append is a slice containing all the elements of the original slice plus the provided values.
- If the backing array of s is too small to fit all the given values a bigger array will be allocated. The returned slice will point to the newly allocated array.
https://go.dev/blog/slices-intro
func Pic(dx, dy int) [][]uint8 {
img := make([][]uint8, dx)
for x := range img {
img[x] = make([]uint8, dy)
for y := range img[x] {
img[x][y] = uint8(x ^ y)
}
}
return img
}
Maps
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.
If the top-level type is just a type name, you can omit it from the elements of the literal.
type Vertex struct {
Lat, Long float64
}
func main() {
var m map[string]Vertex
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
var m2 = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
var m3 = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
}
Operations:
- Insert or update an element in map m:
m[key] = elem
- Retrieve an element:
elem := m[key]
- Access a non-existent key will return zero value of target type.
- Delete an element:
delete(m, key)
- Test that a key is present with a two-value assignment:
elem, ok := m[key]
- If key is in m, ok is true. If not, ok is false.
- If key is not in the map, then elem is the zero value for the map's element type.
Range
The range form of the for loop iterates over a slice or map.
for idx, val := range arr
for idx := range arr
for key, val := range map
for key := range map
Function Values
Functions are values too. They can be passed around just like other values.
Function values may be used as function arguments and return values.
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
Function Closures
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.
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos := adder()
for i := 0; i < 10; i++ {
fmt.Println(pos(i)) // 0 1 3 6 ...
}
}
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.
Remember: a method is just a function with a receiver argument.
You can declare a method on non-struct types, too.
You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int).
You can declare methods with pointer receivers.
Methods with pointer receivers can modify the value to which the receiver points. Since methods often need to modify their receiver, pointer receivers are more common than value receivers.
Methods with value receivers can only modify a copy of the origin value.
Methods can take either a value or a pointer as the receiver when they are called, regardless of the type of its receiver.
In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.
Check this out: https://go.dev/tour/methods/9
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())
}
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 declaration of intent, no "implements" keyword.
Interface values can be thought of as a tuple of a value and a concrete type: (value, 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.
The interface type that specifies zero methods is known as the empty interface: interface{}
.
An empty interface may hold values of any type. (Every type implements at least zero methods.) Empty interfaces are used by code that handles values of unknown type.
package main
import (
"fmt"
"math"
)
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
// Error message:
// ./prog.go:22:6: cannot use v (variable of type Vertex) as type Abser in assignment:
// Vertex does not implement Abser (Abs method has pointer receiver)
// https://go.dev/tour/methods/9
fmt.Println(a.Abs())
}
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)
}
Type Assertions
A type assertion provides access to an interface value's underlying concrete value.
t := i.(T)
- This statement 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.
t, ok := i.(T)
- 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.
- If i holds a T, then t will be the underlying value and ok will be true.
- If not, ok will be false and t will be the zero value of type T, and no panic occurs.
Type Switches
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.
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)
}
}
This switch statement tests whether the interface value i holds a value of type T or S. In each of the T and S cases, the variable v will be of type T or S respectively and hold the value held by i. In the default case (where there is no match), the variable v is of the same interface type and value as i.
TLDR, v's type is not interface anymore if case matched.
Stringers
One of the most ubiquitous interfaces is Stringer defined by the fmt package.
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.
Errors
The error type is a built-in interface similar to fmt.Stringer:
type error interface {
Error() string
}
Readers
The io package specifies the io.Reader interface, which represents the read end of a stream of data.
The io.Reader interface has a Read method:
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.
Generics
Typed Parameters
Go functions can be written to work on multiple types using type parameters. The type parameters of a function appear between brackets, before the function's arguments.
func Index[T comparable](s []T, x T) int
This declaration means that s
is a slice of any type T
that fulfills the built-in constraint comparable
.
x
is also a value of the same type.
comparable
is a useful constraint that makes it possible to use the == and != operators on values of the type.
package main
import "fmt"
// Index returns the index of x in s, or -1 if not found.
func Index[T comparable](s []T, x T) int {
for i, v := range s {
// v and x are type T, which has the comparable
// constraint, so we can use == here.
if v == x {
return i
}
}
return -1
}
func main() {
// Index works on a slice of ints
si := []int{10, 20, 15, -10}
fmt.Println(Index(si, 15))
// Index also works on a slice of strings
ss := []string{"foo", "bar", "baz"}
fmt.Println(Index(ss, "hello"))
}
Generic Types
In addition to generic functions, Go also supports generic types. A type can be parameterized with a type parameter, which could be useful for implementing generic data structures.
package main
import "fmt"
// List represents a singly-linked list that holds
// values of any type.
type List[T any] struct {
next *List[T]
val T
}
func (l *List[T]) getVal() T {
return l.val
}
func main() {
l := List[string]{nil, "ok"}
fmt.Println(l.getVal())
}
Concurrency
Goroutines
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.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Channels
Channels are a typed conduit through which you can send and receive values with the channel operator, <-
.
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.
package main
import "fmt"
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)
}
Buffered Channels
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.
Range and Close
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 c
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.
package main
import (
"fmt"
)
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)
}
}
package main
import (
"golang.org/x/tour/tree"
"fmt"
)
// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
if t == nil {
return
}
Walk(t.Left, ch)
ch <- t.Value
Walk(t.Right, ch)
}
// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
ch1 := make(chan int)
ch2 := make(chan int)
go Walk(t1, ch1)
go Walk(t2, ch2)
for i := 0; i < 10; i ++ {
v1 := <-ch1
v2 := <-ch2
if v1 != v2 {
return false
}
}
return true
}
func main() {
ch := make(chan int)
go Walk(tree.New(1), ch)
for i := 0; i < 10; i ++ {
v := <-ch
fmt.Println(v)
}
fmt.Println(Same(tree.New(1), tree.New(1)))
fmt.Println(Same(tree.New(1), tree.New(2)))
}
Select
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.
package main
import "fmt"
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
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
Default Selection
The default
case in a select
is run if no other case is ready.
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
sync.Mutex
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
, Unlock
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.
If you do not use mutex on Inc, the result are not defined:
fatal error: concurrent map writes
- Correct value
- Not correct value
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.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"))
}
Links: a-tour-of-go