Hello, 世界

欢迎来到Go 编程语言指南.

该指南被分为三节。在每节的末尾有若干个练习需要读者完成。

该指南可以进行交互。点击“Run”按钮(或按 Shift + Enter)可以在远程服务器上你的电脑上编译并执行程序。 结果展示在代码的下面。

这些例子程序展示了 Go 的各个方面。在指南中的程序可以成为你积累经验的开始。

编辑程序并且再次执行它。

当你准备好继续了,点击“Next”按钮或按 PageDown 键。

package main

import "fmt"

func main() {
	fmt.Println("Hello, 世界")
}

Go 教程多语言版本

该指南也有其他语言版本:

(如果你愿意翻译该指南到其他语言,从 https://code.google.com/p/go-tour 检出代码,翻译 static/index.html, 并根据 appengine/README 的介绍将其部署到 App Engine 上。)

点击“Next”按钮或者按 PageDown 继续。

离线 Go 语言教程

在没有互联网接入的情况下该指南可以作为独立的程序使用。

指南独立运行会在本地的设备上构建和编译代码,这运行得更快。并且会包括一些在沙盒版本中没有的体验上的增强。

为了在本地运行指南首先要安装 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 中,首字母大写的名称是被导出的。

FooFOO 都是被导出的名称。 名称 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))
}

For

Go 只有一种循环结构,for 循环。

基本的 for 循环看起来跟 C 或者 Java 中做的一样,除了没有了 ( ) 之外(甚至强制不能使用它们),而 { } 是必须的。

package main

import "fmt"

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}

For

跟 C 或者 Java 中一样,可以让前置、后置语句为空。

package main

import "fmt"

func main() {
	sum := 1
	for ; sum < 1000; {
		sum += sum
	}
	fmt.Println(sum)
}

For

基于此可以省略分号: C 的 while 在 Go 中也是用 for 实现。

package main

import "fmt"

func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}

For

如果省略了循环条件,它就是个死循环源。

package main

func main() {
	for ; ; {
	}
}

For

而为了避免累赘,分号可以省略,因此一个死循环可以简洁地表达。

package main

func main() {
	for {
	}
}

If

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))
}

If

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

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 函数

表达式 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 映射键到值。

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

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)
}

Map

如果顶层类型只有类型名的话,可以在文法的元素中省略键名。

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

在 map m 中插入或修改一个元素:

m[key] = elem

获得元素:

elem = m[key]

删除元素:

delete(m, key)

通过双赋值检测某个键存在:

elem, ok = m[key]

如果 keym 中, oktrue。 否则,okfalse 并且 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

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 可以重新切片,创建一个新的 slice 值指向相同的数组。

表达式

s[lo:hi]

表示从 lohi-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

slice 由函数 make 创建。这会分配一个零长度的数组并且返回一个 slice 指向这个数组:

a := make([]int, 5)  // len(a)=5
slice 有长度和容量。slice 的容量是底层数组可以增长的最大长度。

为了指定容量,可传递第三个参数到 make

b := make([]int, 0, 5) // len(b)=0, cap(b)=5
slice 可以通过“重新切片”来扩容(增长到容量上限):

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

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),
		)
	}
}

Range

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)
	}
}

Range

可以将值赋值给 _ 来忽略序号和值。

如果只需要索引值,去掉“, 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

你可能已经猜到 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 的条件从上到下的执行,当匹配成功的时候停止。

(例如,

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 同 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))
}

练习:Map

实现 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)
}

练习:Slice

实现 Pic。它应当接受一个 slice 的长度 dy,和 slice 中每个元素的长度的 8 位无符号整数 dx。当执行这个程序,它会将整数转换为灰度(好吧,蓝度)图片进行展示。

图片的实现已经完成。可能用到的函数包括 x^y(x+y)/2x*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())
	}
}

进阶练习:复数立方根

让我们通过 complex64complex128 来探索一下 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 作为接收者。

vVertex 的时候 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 定义了 ReaderWriter;不一定要这么做。

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)
	}
}

Web 服务器

包 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.Colorcolor.Model 也是接口,但是通常因为直接使用预定义的实现 image.RGBAColorimage.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))
}

练习:HTTP 处理

实现下面的类型,并在其上定义 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)
}

练习:Rot13 读取器

一般的模式是 io.Reader 包裹另一个 io.Reader,用某些途径修改特定的流。

例如, gzip.NewReader 函数输入一个 io.Reader(gzip 的数据流)并且返回一个同样实现了 io.Reader*gzip.Reader(解压缩后的数据流)。

实现一个实现了 io.Readerrot13Reader,用 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

goroutine 是由 Go 运行时环境管理的轻量级线程。

go f(x, y, z)

开启一个新的 goroutine 执行

f(x, y, z)

fxyz 是当前 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 是有类型的管道,可以用 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

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)
}

Range 和 Close

发送者可以 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

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) 构造了一个随机结构的二叉树,保存了值 k2k3k,...,10k

创建一个新的 channel ch 并且对其进行步进:

go Walk(tree.New(1), ch)

然后从 channel 中读取并且打印 10 个值。应当是值 1,2,3,...,10。

3.Walk 实现 Same 函数来检测是否 t1t2 存储了相同的值。

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() {
}

练习:Web 爬虫

在这个练习中,将会使用 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/",
		},
	},
}

Where to Go from here...

可以从 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 了解更多内容。