欢迎来到Go 编程语言指南.
该指南被分为三节。在每节的末尾有若干个练习需要读者完成。
该指南可以进行交互。点击“Run”按钮(或按 Shift + Enter)可以在远程服务器上你的电脑上编译并执行程序。 结果展示在代码的下面。
这些例子程序展示了 Go 的各个方面。在指南中的程序可以成为你积累经验的开始。
编辑程序并且再次执行它。
当你准备好继续了,点击“Next”按钮或按 PageDown 键。
package main import "fmt" func main() { fmt.Println("Hello, 世界") }
该指南也有其他语言版本:
(如果你愿意翻译该指南到其他语言,从
https://code.google.com/p/go-tour
检出代码,翻译 static/index.html
,
并根据 appengine/README
的介绍将其部署到
App Engine 上。)
点击“Next”按钮或者按 PageDown 继续。
在没有互联网接入的情况下该指南可以作为独立的程序使用。
指南独立运行会在本地的设备上构建和编译代码,这运行得更快。并且会包括一些在沙盒版本中没有的体验上的增强。
为了在本地运行指南首先要安装 Go, 然后使用 go get 安装 gotour:
go get code.google.com/p/go-tour/gotour
也可以安装中文版本:
go get bitbucket.org/mikespook/go-tour-zh/gotour
最后执行安装产生的 gotour
执行文件。
如果不安装本地版本,点击“Next”按钮或者按 PageDown 继续。
(可以在任何时候点击“目录”按钮返回这个介绍。)
每个 Go 程序都是由包组成的。
程序运行的入口是包 main
。
这个程序使用并导入了包
"fmt"
和 "math"
。
按照惯例,包名与导入路径的最后一个目录一致。
package main import ( "fmt" "math" ) func main() { fmt.Println("Happy", math.Pi, "Day") }
这个代码用圆括号组合了导入,这是“factored”导入语句。同样可以编写多个导入语句,例如:
import "fmt" import "math"不过通常都会用 factored 格式来使代码工整。
package main import ( "fmt" "math" ) func main() { fmt.Printf("Now you have %g problems.", math.Nextafter(2, 3)) }
在导入了一个包之后,就可以用其导出的名称来调用它。
在 Go 中,首字母大写的名称是被导出的。
Foo
和 FOO
都是被导出的名称。
名称 foo
是不会被导出的。
执行代码。然后将 math.pi
改名为 math.Pi
再试着执行一下。
package main import ( "fmt" "math" ) func main() { fmt.Println(math.pi) }
函数可以没有参数或接受多个参数。
在这个例子中,add
接受两个 int
类型的参数。
注意类型在变量名之后。
(参考这篇关于 Go 语法定义的文章了解类型以这种形式出现的原因。)
package main import "fmt" func add(x int, y int) int { return x + y } func main() { fmt.Println(add(42, 13)) }
当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
在这个例子中 ,
x int, y int
被缩写为
x, y int
package main import "fmt" func add(x, y int) int { return x + y } func main() { fmt.Println(add(42, 13)) }
函数可以返回任意数量的返回值。
这个函数返回了两个字符串。
package main import "fmt" func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("hello", "world") fmt.Println(a, b) }
函数接受参数。在 Go 中,函数可以返回多个“结果参数”,而不仅仅是一个值。它们可以像变量那样命名和使用。
如果命名了返回值参数,一个没有参数的 return
语句,会将当前的值作为返回值返回。
package main import "fmt" func split(sum int) (x, y int) { x = sum * 4/9 y = sum - x return } func main() { fmt.Println(split(17)) }
var
语句定义了一个变量的列表;跟函数的参数列表一样,类型在后面。
package main import "fmt" var x, y, z int var c, python, java bool func main() { fmt.Println(x, y, z, c, python, java) }
变量定义可以包含初始值,每个变量对应一个。
如果初始化是使用表达式,则可以省略类型;变量从初始值中获得类型。
package main import "fmt" var x, y, z int = 1, 2, 3 var c, python, java = true, false, "no!" func main() { fmt.Println(x, y, z, c, python, java) }
在函数中,:=
简洁赋值语句在明确类型的地方,可以用于替代 var
定义。
(:=
结构不能使用在函数外,函数外的每个语法块都必须以关键字开始。)
package main import "fmt" func main() { var x, y, z int = 1, 2, 3 c, python, java := true, false, "no!" fmt.Println(x, y, z, c, python, java) }
常量的定义与变量类似,只不过使用 const
关键字。
常量可以是字符、字符串、布尔或数字类型的值。
package main import "fmt" const Pi = 3.14 func main() { const World = "世界" fmt.Println("Hello", World) fmt.Println("Happy", Pi, "Day") const Truth = true fmt.Println("Go rules?", Truth) }
数值常量是高精度的值。
一个未指定类型的常量由上下文来决定其类型。
也尝试一下输出 needInt(Big)
吧。
package main import "fmt" const ( Big = 1<<100 Small = Big>>99 ) func needInt(x int) int { return x*10 + 1 } func needFloat(x float64) float64 { return x*0.1 } func main() { fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big)) }
Go 只有一种循环结构,for
循环。
基本的 for
循环看起来跟 C 或者 Java 中做的一样,除了没有了 ( )
之外(甚至强制不能使用它们),而 { }
是必须的。
package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) }
跟 C 或者 Java 中一样,可以让前置、后置语句为空。
package main import "fmt" func main() { sum := 1 for ; sum < 1000; { sum += sum } fmt.Println(sum) }
基于此可以省略分号:
C 的 while
在 Go 中也是用 for
实现。
package main import "fmt" func main() { sum := 1 for sum < 1000 { sum += sum } fmt.Println(sum) }
如果省略了循环条件,它就是个死循环源。
package main func main() { for ; ; { } }
而为了避免累赘,分号可以省略,因此一个死循环可以简洁地表达。
package main func main() { for { } }
if
语句看起来跟 C 或者 Java 中的一样,除了没有了 ( )
之外(甚至强制不能使用它们),而 { }
是必须的。
(耳熟吗?)
package main import ( "fmt" "math" ) func sqrt(x float64) string { if x < 0 { return sqrt(-x) + "i" } return fmt.Sprint(math.Sqrt(x)) } func main() { fmt.Println(sqrt(2), sqrt(-4)) }
跟 for
一样,if
语句可以在条件之前执行一个简单的语句。
由这个语句定义的变量的作用域仅在 if
范围之内。
(在最后的 return
语句处使用 v
看看。)
package main import ( "fmt" "math" ) func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } return lim } func main() { fmt.Println( pow(3, 2, 10), pow(3, 3, 20), ) }
在 if
的简单语句处定义的变量同样可以在任何对应的 else
块中使用。
package main import ( "fmt" "math" ) func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } else { fmt.Printf("%g >= %g\n", v, lim) } // 不能在这里使用 v,因此 return lim } func main() { fmt.Println( pow(3, 2, 10), pow(3, 3, 20), ) }
Go 的基本类型有
bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // uint8 的别名 rune // int32 的别名 // 代表一个Unicode码点 float32 float64 complex64 complex128
package main import ( "math/cmplx" "fmt" ) var ( ToBe bool = false MaxInt uint64 = 1<<64 - 1 z complex128 = cmplx.Sqrt(-5+12i) ) func main() { const f = "%T(%v)\n" fmt.Printf(f, ToBe, ToBe) fmt.Printf(f, MaxInt, MaxInt) fmt.Printf(f, z, z) }
一个结构体(struct
)就是一个字段的集合。
(而 type
定义跟其字面意思相符。)
package main import "fmt" type Vertex struct { X int Y int } func main() { fmt.Println(Vertex{1, 2}) }
结构体字段使用点号来访问。
package main import "fmt" type Vertex struct { X int Y int } func main() { v := Vertex{1, 2} v.X = 4 fmt.Println(v.X) }
Go 有指针,但是没有指针运算。
结构体字段可以通过结构体指针来访问。通过指针间接的访问是透明的。
package main import "fmt" type Vertex struct { X int Y int } func main() { p := Vertex{1, 2} q := &p q.X = 1e9 fmt.Println(p) }
结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。
使用 Name:
语法可以仅列出部分字段。(字段名的顺序无关。)
特殊的前缀 &
构造了指向结构体文法的指针。
package main import "fmt" type Vertex struct { X, Y int } var ( p = Vertex{1, 2} // has type Vertex q = &Vertex{1, 2} // has type *Vertex r = Vertex{X: 1} // Y:0 is implicit s = Vertex{} // X:0 and Y:0 ) func main() { fmt.Println(p, q, r, s) }
表达式 new(T)
分配了一个零初始化的 T
值,并返回指向它的指针。
var t *T = new(T)
或
t := new(T)
package main import "fmt" type Vertex struct { X, Y int } func main() { v := new(Vertex) fmt.Println(v) v.X, v.Y = 11, 9 fmt.Println(v) }
map 映射键到值。
map 在使用之前必须用 make
来创建(不是 new
);一个值为 nil
的 map 是空的,并且不能赋值。
package main import "fmt" type Vertex struct { Lat, Long float64 } var m map[string]Vertex func main() { m = make(map[string]Vertex) m["Bell Labs"] = Vertex{ 40.68433, 74.39967, } fmt.Println(m["Bell Labs"]) }
map 的文法跟结构体文法相似,不过键名是必须的。
package main import "fmt" type Vertex struct { Lat, Long float64 } var m = map[string]Vertex{ "Bell Labs": Vertex{ 40.68433, -74.39967, }, "Google": Vertex{ 37.42202, -122.08408, }, } func main() { fmt.Println(m) }
如果顶层类型只有类型名的话,可以在文法的元素中省略键名。
package main import "fmt" type Vertex struct { Lat, Long float64 } var m = map[string]Vertex{ "Bell Labs": {40.68433, -74.39967}, "Google": {37.42202, -122.08408}, } func main() { fmt.Println(m) }
在 map m
中插入或修改一个元素:
m[key] = elem
获得元素:
elem = m[key]
删除元素:
delete(m, key)
通过双赋值检测某个键存在:
elem, ok = m[key]
如果 key
在 m
中,
ok
是 true
。
否则,ok
是 false
并且
elem
是 map 的元素类型的零值。
同样的,当从 map 中读取某个不存在的键时,结果是 map 的元素类型的零值。
package main import "fmt" func main() { m := make(map[string]int) m["Answer"] = 42 fmt.Println("The value:", m["Answer"]) m["Answer"] = 48 fmt.Println("The value:", m["Answer"]) delete(m, "Answer") fmt.Println("The value:", m["Answer"]) v, ok := m["Answer"] fmt.Println("The value:", v, "Present?", ok) }
slice 指向数组的值,并且同时包含了长度信息。
[]T
是一个元素类型为 T
的 slice。
package main import "fmt" func main() { p := []int{2, 3, 5, 7, 11, 13} fmt.Println("p ==", p) for i := 0; i < len(p); i++ { fmt.Printf("p[%d] == %d\n", i, p[i]) } }
slice 可以重新切片,创建一个新的 slice 值指向相同的数组。
表达式
s[lo:hi]
表示从 lo
到 hi-1
的 slice 元素,含有两端。
因此
s[lo:lo]
是空的,而
s[lo:lo+1]
有一个元素。
package main import "fmt" func main() { p := []int{2, 3, 5, 7, 11, 13} fmt.Println("p ==", p) fmt.Println("p[1:4] ==", p[1:4]) // missing low index implies 0 fmt.Println("p[:3] ==", p[:3]) // missing high index implies len(s) fmt.Println("p[4:] ==", p[4:]) }
slice 由函数 make
创建。这会分配一个零长度的数组并且返回一个 slice 指向这个数组:
a := make([]int, 5) // len(a)=5slice 有长度和容量。slice 的容量是底层数组可以增长的最大长度。
为了指定容量,可传递第三个参数到 make
:
b := make([]int, 0, 5) // len(b)=0, cap(b)=5slice 可以通过“重新切片”来扩容(增长到容量上限):
b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=4
package main import "fmt" func main() { a := make([]int, 5) printSlice("a", a) b := make([]int, 0, 5) printSlice("b", b) c := b[:2] printSlice("c", c) d := c[2:5] printSlice("d", d) } func printSlice(s string, x []int) { fmt.Printf("%s len=%d cap=%d %v\n", s, len(x), cap(x), x) }
slice 的零值是 nil
。
一个 nil 的 slice 的长度和容量是 0。
(了解更多关于 slice 的内容,参阅文章 “Slices:使用和内幕”。)
package main import "fmt" func main() { var z []int fmt.Println(z, len(z), cap(z)) if z == nil { fmt.Println("nil!") } }
函数也是值。
package main import ( "fmt" "math" ) func main() { hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(3, 4)) }
并且函数是完全闭包的。
函数 adder
返回一个闭包。每个闭包被绑定到了特定的 sum
变量上。
package main import "fmt" 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), ) } }
for
循环的 range
格式可以对 slice 或者 map 进行迭代循环。
package main import "fmt" var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} func main() { for i, v := range pow { fmt.Printf("2**%d = %d\n", i, v) } }
可以将值赋值给 _
来忽略序号和值。
如果只需要索引值,去掉“, value
”的部分即可。
package main import "fmt" func main() { pow := make([]int, 10) for i := range pow { pow[i] = 1<<uint(i) } for _, value := range pow { fmt.Printf("%d\n", value) } }
你可能已经猜到 switch
可能的形式了。
case 体会自动终止,除非用 fallthrough
语句作为结尾。
package main 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.", os) } }
switch 的条件从上到下的执行,当匹配成功的时候停止。
(例如,
switch i { case 0: case f(): }
当 i==0
时不会调用 f
。)
package main import ( "fmt" "time" ) func main() { fmt.Println("When's Saturday?") today := time.Now().Weekday() switch time.Saturday { case today+0: fmt.Println("Today.") case today+1: fmt.Println("Tomorrow.") case today+2: fmt.Println("In two days.") default: fmt.Println("Too far away.") } }
没有条件的 switch 同 switch true
一样。
这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。
package main 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.") } }
作为练习函数和循环的简单途径,用牛顿法实现开方函数。
在这个例子中,牛顿法是通过选择一个初始点 z 然后重复这一过程求 Sqrt(x)
的近似值:
为了做到这个,只需要重复计算 10 次,并且观察不同的值(1,2,3,……)是如何逐步逼近结果的。
然后,修改循环条件,使得当值停止改变(或改变非常小)的时候退出循环。观察迭代次数是否变化。结果与 math.Sqrt 接近吗?
提示:定义并初始化一个浮点值,向其提供一个浮点语法或使用转换:
z := float64(1) z := 1.0
package main import ( "fmt" ) func Sqrt(x float64) float64 { } func main() { fmt.Println(Sqrt(2)) }
实现 WordCount
。它应当返回一个含有 s
中每个 “word” 数量的 map。函数 wc.Test
针对这个函数执行一个测试用例,并打印成功或者失败。
你会发现 strings.Fields 很有帮助。
package main import ( "tourbitbucket.org/mikespook/go-tour-zh/wc" ) func WordCount(s string) map[string]int { return map[string]int{"x": 1} } func main() { wc.Test(WordCount) }
实现 Pic
。它应当接受一个 slice
的长度 dy
,和 slice 中每个元素的长度的 8 位无符号整数
dx
。当执行这个程序,它会将整数转换为灰度(好吧,蓝度)图片进行展示。
图片的实现已经完成。可能用到的函数包括 x^y
,(x+y)/2
和 x*y
。
(需要使用循环来分配 [][]uint8
中的每个 []uint8
。)
(使用 uint8(intValue)
在类型之间转换。)
package main import "tourbitbucket.org/mikespook/go-tour-zh/pic" func Pic(dx, dy int) [][]uint8 { } func main() { pic.Show(Pic) }
现在来通过函数找些乐趣。
实现一个 fibonacci
函数,返回一个函数(一个闭包)可以返回连续的斐波纳契数。
package main import "fmt" // fibonacci 函数会返回一个返回 int 的函数。 func fibonacci() func() int { } func main() { f := fibonacci() for i := 0; i < 10; i++ { fmt.Println(f()) } }
让我们通过 complex64
和 complex128
来探索一下 Go 内建的复数。
对于立方根,牛顿法需要大量循环:
找到 2 的立方根,确保算法能够工作。在 math/cmplx
包中有
Pow 函数。
package main import "fmt" func Cbrt(x complex128) complex128 { } func main() { fmt.Println(Cbrt(2)) }
Go 没有类。然而,仍然可以在结构体类型上定义方法。
方法接收者出现在 func
关键字和方法名之间的参数中。
package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := &Vertex{3, 4} fmt.Println(v.Abs()) }
事实上,可以对包中的任意类型定义任意方法,而不仅仅是结构体。
不能对来自其他包的类型或基础类型定义方法。
package main import ( "fmt" "math" ) type MyFloat float64 func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) } func main() { f := MyFloat(-math.Sqrt2) fmt.Println(f.Abs()) }
方法可以与命名类型或命名类型的指针关联。
刚刚看到的两个 Abs
方法。一个是在 *Vertex
指针类型上,而另一个在 MyFloat
值类型上。
有两个原因需要使用指针接收者。首先避免在每个方法调用中拷贝值(如果值类型是大的结构体的话会更有效率)。其次,方法可以修改接收者指向的值。
尝试修改 Abs
的定义,同时 Scale
方法使用 Vertex
代替 *Vertex
作为接收者。
当 v
是 Vertex
的时候 Scale
方法没有任何作用。Scale
修改 v
。当 v
是一个值(非指针),方法看到的是 Vertex
的副本,并且无法修改原始值。
Abs
的工作方式是一样的。只不过,仅仅读取 v
。所以读取的是原始值(通过指针)还是那个值的副本并没有关系。
package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f } func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := &Vertex{3, 4} v.Scale(5) fmt.Println(v, v.Abs()) }
接口类型是由一组方法定义的集合。
接口类型的值可以存放实现这些方法的任何值。
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 a = v // a Vertex, does NOT // implement Abser 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) }
类型通过实现那些方法来实现接口。
没有显式声明的必要。
隐式接口解藕了实现接口的包和定义接口的包:互不依赖。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。(对比其他语言)
包 io 定义了 Reader
和 Writer
;不一定要这么做。
package main import ( "fmt" "os" ) type Reader interface { Read(b []byte) (n int, err error) } type Writer interface { Write(b []byte) (n int, err error) } type ReadWriter interface { Reader Writer } func main() { var w Writer // os.Stdout implements Writer w = os.Stdout fmt.Fprintf(w, "hello, writer\n") }
错误是可以用字符串描述自己的任何东西。
主要思路是由预定义的内建接口类型
error
,和其返回返回字符串窜的方法 Error
构成。
type error interface { Error() string }
当用 fmt
包的多种不同的打印函数输出一个 error
时,会自动的调用该方法。
package main import ( "fmt" "time" ) type MyError struct { When time.Time What string } func (e *MyError) Error() string { return fmt.Sprintf("at %v, %s", e.When, e.What) } func run() error { return &MyError{ time.Now(), "it didn't work", } } func main() { if err := run(); err != nil { fmt.Println(err) } }
包 http
通过任何实现了 http.Handler
的值来响应 HTTP 请求:
package http type Handler interface { ServeHTTP(w ResponseWriter, r *Request) }
在这个例子中,类型 Hello
实现了 http.Handler
。
访问 http://localhost:4000/ 会看到来自程序的问候。 注意: 这个例子无法在基于 web 的指南用户界面运行。为了尝试编写 web 服务器,可能需要安装 Go。
package main import ( "fmt" "net/http" ) type Hello struct{} func (h Hello) ServeHTTP( w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello!") } func main() { var h Hello http.ListenAndServe("localhost:4000",h) }
Package image 定义了 Image
接口:
package image type Image interface { ColorModel() color.Model Bounds() Rectangle At(x, y int) color.Color }
(参阅文档了解全部信息。)
同样,color.Color
和 color.Model
也是接口,但是通常因为直接使用预定义的实现 image.RGBAColor
和 image.RGBAColorModel
而忽略它们。
package main import ( "fmt" "image" ) func main() { m := image.NewRGBA(image.Rect(0, 0, 100, 100)) fmt.Println(m.Bounds()) fmt.Println(m.At(0, 0).RGBA()) }
从之前的练习中复制 Sqrt
函数,并修改使其返回 error
值。
Sqrt
接收到一个负数时,应当返回一个非 nil 的错误值。复数同样也不被支持。
创建一个新类型
type ErrNegativeSqrt float64
使其成为一个 error
,通过
func (e ErrNegativeSqrt) Error() string
方法就可以让 ErrNegativeSqrt(-2).Error()
返回
"cannot Sqrt negative number: -2"
。
注意: 在 Error
方法内调用 fmt.Print(e)
将会让程序陷入死循环。
可以通过先转换 e
来避免这个:
fmt.Print(float64(e))
。为什么?
修改 Sqrt
函数,使其接受一个负数时,返回 ErrNegativeSqrt
值。
package main import ( "fmt" ) func Sqrt(f float64) (float64, error) { return 0, nil } func main() { fmt.Println(Sqrt(2)) fmt.Println(Sqrt(-2)) }
实现下面的类型,并在其上定义 ServeHTTP 方法。 在 web 服务器中注册它们来处理指定的路径。
type String string type Struct struct { Greeting string Punct string Who string }
例如,可以使用如下方式注册处理方法:
http.Handle("/string", String("I'm a frayed knot.")) http.Handle("/struct", &Struct{"Hello", ":", "Gophers!"})
package main import ( "net/http" ) func main() { // your http.Handle calls here http.ListenAndServe("localhost:4000", nil) }
还记得之前编写的图片生成器吗?
现在来编写另一个,不过这次将会返回一个 image.Image
的实现来代替 slice 的数据。
自定义的 Image
类型,要实现必要的方法,并且调用 pic.ShowImage
。
Bounds
应当返回一个 image.Rectangle
,例如 image.Rect(0, 0, w, h)
。
ColorModel
应当返回 image.RGBAColorModel
。
At
应当返回一个颜色;在这个例子里,在最后一个图片生成器的值 v
匹配 color.RGBAColor{v, v, 255, 255}
。
package main import ( "image" "tourbitbucket.org/mikespook/go-tour-zh/pic" ) type Image struct{} func main() { m := Image{} pic.ShowImage(m) }
一般的模式是 io.Reader 包裹另一个 io.Reader
,用某些途径修改特定的流。
例如, gzip.NewReader
函数输入一个 io.Reader
(gzip 的数据流)并且返回一个同样实现了 io.Reader
的
*gzip.Reader
(解压缩后的数据流)。
实现一个实现了 io.Reader
的 rot13Reader
,用
ROT13
修改数据流中的所有的字母进行密文替换。
rot13Reader
已经提供。通过实现其 Read
方法使得它匹配 io.Reader
。
package main import ( "io" "os" "strings" ) type rot13Reader struct { r io.Reader } func main() { s := strings.NewReader( "Lbh penpxrq gur pbqr!") r := rot13Reader{s} io.Copy(os.Stdout, &r) }
goroutine 是由 Go 运行时环境管理的轻量级线程。
go f(x, y, z)
开启一个新的 goroutine 执行
f(x, y, z)
f
,x
,y
和 z
是当前
goroutine 中定义的,但是在新的 goroutine 中运行 f
。
goroutine 在相同的地址空间中运行,因此访问共享内存必须进行同步。
sync
提供了这种可能,不过在 Go 中并不经常用到,因为有其他的办法。 (在接下来的内容中会涉及到。)
package main import ( "fmt" "runtimetime" ) func say(s string) { for i := 0; i < 5; i++ { runtime.Gosched()time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") say("hello") }
channel 是有类型的管道,可以用 channel 操作符 <-
对其发送或者接收值。
ch <- v // 将 v 送入 channel ch。 v := <-ch // 从 ch 接收,并且赋值给 v。
(“箭头”就是数据流的方向。)
和 map 与 slice 一样,channel 使用前必须创建:
ch := make(chan int)
默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。
package main import "fmt" func sum(a []int, c chan int) { sum := 0 for _, v := range a { sum += v } c <- sum // send sum to c } func main() { a := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(a[:len(a)/2], c) go sum(a[len(a)/2:], c) x, y := <-c, <-c // 从 c 中接收 fmt.Println(x, y, x + y) }
channel 可以是带缓冲的。为 make
提供第二个参数作为缓冲长度来初始化一个缓冲 channel:
ch := make(chan int, 100)
向缓冲 channel 发送数据的时候,只有在缓冲区满的时候才会阻塞。当缓冲区清空的时候接受阻塞。
修改例子使得缓冲区被填满,然后看看会发生什么。
package main import "fmt" func main() { c := make(chan int, 2) c <- 1 c <- 2 fmt.Println(<-c) fmt.Println(<-c) }
发送者可以 close
一个 channel 来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试
channel 是否被关闭:当没有值可以接收并且 channel 已经被关闭,那么
v, ok := <-ch
ok
会被设置为 false
。
循环 for i := range c
会不断从 channel 接收值,直到它被关闭。
注意: 只有发送者才能关闭 channel,而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。
还要注意:channel 与文件不同;通常情况下无需关闭它们。只有在需要告诉接收者没有更多的数据的时候才有必要进行关闭,例如中断一个 range
。
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) } }
select
语句使得一个 goroutine 在多个通讯操作上等待。
select
会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。
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) }
当 select
中的其他条件分支都没有准备好的时候,default
分支会被执行。
为了非阻塞的发送或者接收,可使用 default
分支:
select { case i := <-c: // use i default: // receiving from c would block }
注意: 这个例子无法在指南的基于 web 的用户界面中运行,因为沙盒环境是没有定时器概念的。需要安装、 Go 来了解这个例子如何运行。
package main import ( "fmt" "time" ) func main() { tick := time.Tick(1e8) boom := time.After(5e8) for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") return default: fmt.Println(" .") time.Sleep(5e7) } } }
可以用多种不同的二叉树的叶子节点存储相同的数列值。例如,这里有两个二叉树保存了序列 1,1,2,3,5,8,13。
用于检查两个二叉树是否存储了相同的序列的函数在多数语言中都是相当复杂的。这里将使用 Go 的并发和 channel 来编写一个简单的解法。
这个例子使用了 tree
包,定义了类型:
type Tree struct { Left *Tree Value int Right *Tree }
1. 实现 Walk
函数。
2. 测试 Walk
函数。
函数 tree.New(k)
构造了一个随机结构的二叉树,保存了值
k
,2k
,3k
,...,10k
。
创建一个新的 channel ch
并且对其进行步进:
go Walk(tree.New(1), ch)
然后从 channel 中读取并且打印 10 个值。应当是值 1,2,3,...,10。
3. 用 Walk
实现 Same
函数来检测是否 t1
和 t2
存储了相同的值。
4. 测试 Same
函数。
Same(tree.New(1), tree.New(1))
应当返回 true,而
Same(tree.New(1), tree.New(2))
应当返回 false。
package main import "tourbitbucket.org/mikespook/go-tour-zh/tree" // Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。 func Walk(t *tree.Tree, ch chan int) // Same 检测树 t1 和 t2 是否含有相同的值。 func Same(t1, t2 *tree.Tree) bool func main() { }
在这个练习中,将会使用 Go 的并发特性来并行执行 web 爬虫。
修改 Crawl
函数来并行的抓取 URLs,并且保证不重复。
package main import ( "fmt" ) type Fetcher interface { // Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。 Fetch(url string) (body string, urls []string, err error) } // Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。 func Crawl(url string, depth int, fetcher Fetcher) { // TODO: 并行的抓取 URL。 // TODO: 不重复抓取页面。 // 下面并没有实现上面两种情况: if depth <= 0 { return } body, urls, err := fetcher.Fetch(url) if err != nil { fmt.Println(err) return } fmt.Printf("found: %s %q\n", url, body) for _, u := range urls { Crawl(u, depth-1, fetcher) } return } func main() { Crawl("http://golang.org/", 4, fetcher) } // fakeFetcher 是返回若干结果的 Fetcher。 type fakeFetcher map[string]*fakeResult type fakeResult struct { body string urls []string } func (f *fakeFetcher) Fetch(url string) (string, []string, error) { if res, ok := (*f)[url]; ok { return res.body, res.urls, nil } return "", nil, fmt.Errorf("not found: %s", url) } // fetcher 是填充后的 fakeFetcher。 var fetcher = &fakeFetcher{ "http://golang.org/": &fakeResult{ "The Go Programming Language", []string{ "http://golang.org/pkg/", "http://golang.org/cmd/", }, }, "http://golang.org/pkg/": &fakeResult{ "Packages", []string{ "http://golang.org/", "http://golang.org/cmd/", "http://golang.org/pkg/fmt/", "http://golang.org/pkg/os/", }, }, "http://golang.org/pkg/fmt/": &fakeResult{ "Package fmt", []string{ "http://golang.org/", "http://golang.org/pkg/", }, }, "http://golang.org/pkg/os/": &fakeResult{ "Package os", []string{ "http://golang.org/", "http://golang.org/pkg/", }, }, }
可以从 installing Go 或者下载 Go App Engine SDK。
一旦在机器上安装好了 Go, Go Documentation 是应当继续阅读的内容 作为开始。 它包含了参考、指南、视频等等更多资料。
在标准库上需要帮助的话,参考 package reference。语言本身的帮助,可以阅读令人愉快的 Language Spec。
进一步探索 Go 的并发模型,参阅 Share Memory by Communicating 的代码之旅。
First Class Functions in Go 为 Go 的函数类型提供了有趣的展示。
Go Blog 有着众多的关于 Go 的文章信息。
访问 golang.org 了解更多内容。