<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>Go 指南</title> <!-- jQuery --> <script src="static/jquery.js"></script> <!-- CodeMirror --> <link rel="stylesheet" href="/static/codemirror/lib/codemirror.css"> <script src="/static/codemirror/lib/codemirror.js"></script> <script src="/static/codemirror/lib/go.js"></script> <!-- Tour --> <link rel="stylesheet" href="static/tour.css"> <script src="static/mode.js"></script> <script src="static/tour.js"></script> </head> <body> <div id="wrap"> <div id="header"> <div id="slidenum">1</div> <a href="#toc" id="tocbtn" title="目录"></a> <h1>Go 指南</h1> </div> <div id="slides" class="slides"><!-- begin slides --> <div class="slide"> <h2>Hello, 世界</h2> <p> 欢迎来到<a target="_blank" href="http://golang.org/">Go 编程语言</a>指南. <p> 该指南被分为三节。在每节的末尾有若干个练习需要读者完成。 <p> 该指南可以进行交互。点击“Run”按钮(或按 Shift + Enter)可以在<span class="appengineMode">远程服务器上</span><span class="localMode">你的电脑上</span>编译并执行程序。 结果展示在代码的下面。 <p> 这些例子程序展示了 Go 的各个方面。在指南中的程序可以成为你积累经验的开始。 <p> 编辑程序并且再次执行它。 <p> 当你准备好继续了,点击“Next”按钮或按 PageDown 键。 <pre class="source">package main import "fmt" func main() { fmt.Println("Hello, 世界") }</pre> </div> <div class="slide nocode appengineMode"> <h2>Go 本地化</h2> <p> 该指南也有其他语言版本: <ul> <li><a href="http://go-tour-br.appspot.com/">Brazilian Portuguese — Português do Brasil</a></li> <li><a href="http://go-tour-zh.appspot.com/">Chinese — 普通话</a></li> <li><a href="http://go-tour-jp.appspot.com/">Japanese — 日本語</a></li> </ul> <p> (如果你愿意翻译该指南到其他语言,从 <code>https://code.google.com/p/go-tour</code> 检出代码,翻译 <code>static/index.html</code>, 并根据 <code>appengine/README</code> 的介绍将其部署到 App Engine 上。) <p> 点击“Next”按钮或者按 PageDown 继续。 </div> <div class="slide nocode appengineMode"> <h2>离线 Go 指南</h2> <p> 在没有互联网接入的情况下该指南可以作为独立的程序使用。 <p> 指南独立运行会在本地的设备上构建和编译代码,这运行得更快。并且会包括一些在沙盒版本中没有的体验上的增强。 <p> 为了在本地运行指南首先要<a target="_blank" href="http://golang.org/doc/install/">安装 Go</a>, 然后使用 <a target="_blank" href="http://golang.org/cmd/go/">go get</a> 安装 <a target="_blank" href="http://code.google.com/p/go-tour/">gotour</a>: <pre> go get code.google.com/p/go-tour/gotour</pre> <p> 也可以安装<a target="_blank" href="http://bitbucket.org/mikespook/go-tour-zh/">中文版本</a>: <pre> go get bitbucket.org/mikespook/go-tour-zh/gotour</pre> <p> 最后执行安装产生的 <code>gotour</code> 执行文件。 <p> 如果不安装本地版本,点击“Next”按钮或者按 PageDown 继续。 <p> <i>(可以在任何时候点击“目录”按钮返回这个介绍。)</i> </div> <div class="toc">简介</div> <div class="slide"> <h2>包</h2> <p> 每个 Go 程序都是由包组成的。 <p> 程序运行的入口是包 <code>main</code>。 <p> 这个程序使用并导入了包 <code>"fmt"</code> 和 <code>"math"</code>。 <p> 按照惯例,包名与导入路径的最后一个目录一致。 <pre class="source">package main import ( "fmt" "math" ) func main() { fmt.Println("Happy", math.Pi, "Day") }</pre> </div> <div class="slide"> <h2>导入</h2> <p> 这个代码用圆括号组合了导入,这是“factored”导入语句。同样可以编写多个导入语句,例如: <pre>import "fmt" import "math"</pre> 不过通常都会用 factored 格式来使代码工整。 <pre class="source">package main import ( "fmt" "math" ) func main() { fmt.Printf("Now you have %g problems.", math.Nextafter(2, 3)) }</pre> </div> <div class="slide"> <h2>导出名</h2> <p> 在导入了一个包之后,就可以用其导出的名称来调用它。 <p> 在 Go 中,首字母大写的名称是被导出的。 <p> <code>Foo</code> 和 <code>FOO</code> 都是被导出的名称。 名称 <code>foo</code> 是不会被导出的。 <p> 执行代码。然后将 <code>math.pi</code> 改名为 <code>math.Pi</code> 再试着执行一下。 <pre class="source">package main import ( "fmt" "math" ) func main() { fmt.Println(math.pi) }</pre> </div> <div class="slide"> <h2>函数</h2> <p> 函数可以没有参数或接受多个参数。 <p> 在这个例子中,<code>add</code> 接受两个 <code>int</code> 类型的参数。 <p> 注意类型在变量名<i>之后</i>。 <p> (参考<a target="_blank" href="http://golang.org/doc/articles/gos_declaration_syntax.html">这篇关于 Go 语法定义的文章</a>了解类型以这种形式出现的原因。) <pre class="source">package main import "fmt" func add(x int, y int) int { return x + y } func main() { fmt.Println(add(42, 13)) }</pre> </div> <div class="slide"> <h2>函数</h2> <p> 当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。 <p> 在这个例子中 , <pre>x int, y int</pre> <p> 被缩写为 <p> <pre>x, y int</pre> <pre class="source">package main import "fmt" func add(x, y int) int { return x + y } func main() { fmt.Println(add(42, 13)) }</pre> </div> <div class="slide"> <h2>函数</h2> <p> 函数可以返回任意数量的返回值。 <p> 这个函数返回了两个字符串。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>函数</h2> <p> 函数接受参数。在 Go 中,函数可以返回多个“结果参数”,而不仅仅是一个值。它们可以像变量那样命名和使用。 <p> 如果命名了返回值参数,一个没有参数的 <code>return</code> 语句,会将当前的值作为返回值返回。 <pre class="source">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)) }</pre> </div> <div class="slide"> <h2>变量</h2> <p> <code>var</code> 语句定义了一个变量的列表;跟函数的参数列表一样,类型在后面。 <pre class="source">package main import "fmt" var x, y, z int var c, python, java bool func main() { fmt.Println(x, y, z, c, python, java) }</pre> </div> <div class="slide"> <h2>变量</h2> <p> 变量定义可以包含初始值,每个变量对应一个。 <p> 如果初始化是使用表达式,则可以省略类型;变量从初始值中获得类型。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>变量</h2> <p> 在函数中,<code>:=</code> 简洁赋值语句在明确类型的地方,可以用于替代 <code>var</code> 定义。 <p> (<code>:=</code> 结构不能使用在函数外,函数外的每个语法块都必须以关键字开始。) <pre class="source">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) }</pre> </div> <div class="slide"> <h2>常量</h2> <p> 常量的定义与变量类似,只不过使用 <code>const</code> 关键字。 <p> 常量可以是字符、字符串、布尔或数字类型的值。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>数值常量</h2> <p> 数值常量是高精度的<i>值</i>。 <p> 一个未指定类型的常量由上下文来决定其类型。 <p> 也尝试一下输出 <code>needInt(Big)</code>吧。 <pre class="source">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)) }</pre> </div> <div class="slide"> <h2>For</h2> <p> Go 只有一种循环结构,<code>for</code> 循环。 <p> 基本的 <code>for</code> 循环看起来跟 C 或者 Java 中做的一样,除了没有了 <code>( )</code> 之外(甚至强制不能使用它们),而 <code>{ }</code> 是必须的。 <pre class="source">package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) }</pre> </div> <div class="slide"> <h2>For</h2> <p> 跟 C 或者 Java 中一样,可以让前置、后置语句为空。 <pre class="source">package main import "fmt" func main() { sum := 1 for ; sum < 1000; { sum += sum } fmt.Println(sum) }</pre> </div> <div class="slide"> <h2>For</h2> <p> 基于此可以省略分号: C 的 <code>while</code> 在 Go 中也是用 <code>for</code> 实现。 <pre class="source">package main import "fmt" func main() { sum := 1 for sum < 1000 { sum += sum } fmt.Println(sum) }</pre> </div> <div class="slide"> <h2>For</h2> <p> 如果省略了循环条件,它就是个死循环源。 <pre class="source">package main func main() { for ; ; { } }</pre> </div> <div class="slide"> <h2>For</h2> <p> 而为了避免累赘,分号可以省略,因此一个死循环可以简洁地表达。 <pre class="source">package main func main() { for { } }</pre> </div> <div class="slide"> <h2>If</h2> <p> <code>if</code> 语句看起来跟 C 或者 Java 中的一样,除了没有了 <code>( )</code> 之外(甚至强制不能使用它们),而 <code>{ }</code> 是必须的。 <p> (耳熟吗?) <pre class="source">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)) }</pre> </div> <div class="slide"> <h2>If</h2> <p> 跟 <code>for</code> 一样,<code>if</code> 语句可以在条件之前执行一个简单的语句。 <p> 由这个语句定义的变量的作用域仅在 <code>if</code> 范围之内。 <p> (在最后的 <code>return</code> 语句处使用 <code>v</code> 看看。) <pre class="source">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), ) }</pre> </div> <div class="slide"> <h2>If</h2> <p> 在 <code>if</code> 的简单语句处定义的变量同样可以在任何对应的 <code>else</code> 块中使用。 <pre class="source">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), ) }</pre> </div> <div class="slide"> <h2>基本类型</h2> <p> Go 的基本类型有 <pre>bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // uint8 的别名 rune // int32 的别名 // 代表一个Unicode码点 float32 float64 complex64 complex128</pre> <pre class="source">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) }</pre> </div> <div class="slide"> <h2>结构体</h2> <p> 一个结构体(<code>struct</code>)就是一个字段的集合。 <p> (而 <code>type</code> 定义跟其字面意思相符。) <pre class="source">package main import "fmt" type Vertex struct { X int Y int } func main() { fmt.Println(Vertex{1, 2}) }</pre> </div> <div class="slide"> <h2>结构体字段</h2> <p> 结构体字段使用点号来访问。 <pre class="source">package main import "fmt" type Vertex struct { X int Y int } func main() { v := Vertex{1, 2} v.X = 4 fmt.Println(v.X) }</pre> </div> <div class="slide"> <h2>指针</h2> <p> Go 有指针,但是没有指针运算。 <p> 结构体字段可以通过结构体指针来访问。通过指针间接的访问是透明的。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>结构体文法</h2> <p> 结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。 <p> 使用 <code>Name:</code> 语法可以仅列出部分字段。(字段名的顺序无关。) <p> 特殊的前缀 <code>&</code> 构造了指向结构体文法的指针。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>new 函数</h2> <p> 表达式 <code>new(T)</code> 分配了一个零初始化的 <code>T</code> 值,并返回指向它的指针。 <pre>var t *T = new(T)</pre> <p> 或 <pre>t := new(T)</pre> <pre class="source">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) }</pre> </div> <div class="slide"> <h2>Map</h2> <p> map 映射键到值。 <p> <!-- TODO: empty part not true in compilers yet --> map 在使用之前必须用 <code>make</code> 来创建(不是 <code>new</code>);一个值为 <code>nil</code> 的 map 是空的,并且不能赋值。 <pre class="source">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"]) }</pre> </div> <div class="slide"> <h2>Map</h2> <p> map 的文法跟结构体文法相似,不过键名是必须的。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>Map</h2> <p> 如果顶层类型只有类型名的话,可以在文法的元素中省略键名。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>修改 map</h2> <p> 在 map <code>m</code> 中插入或修改一个元素: <pre>m[key] = elem</pre> <p> 获得元素: <pre>elem = m[key]</pre> <p> 删除元素: <pre>delete(m, key)</pre> <p> 通过双赋值检测某个键存在: <pre>elem, ok = m[key]</pre> <p> 如果 <code>key</code> 在 <code>m</code> 中, <code>ok</code> 是 <code>true</code>。 否则,<code>ok</code> 是 <code>false</code> 并且 <code>elem</code> 是 map 的元素类型的零值。 <p> 同样的,当从 map 中读取某个不存在的键时,结果是 map 的元素类型的零值。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>slice</h2> <p> slice 指向数组的值,并且同时包含了长度信息。 <p> <code>[]T</code> 是一个元素类型为 <code>T</code> 的 slice。 <pre class="source">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]) } }</pre> </div> <div class="slide"> <h2>Slice</h2> <p> slice 可以重新切片,创建一个新的 slice 值指向相同的数组。 <p> 表达式 <pre>s[lo:hi]</pre> <p> 表示从 <code>lo</code> 到 <code>hi-1</code> 的 slice 元素,含有两端。 因此 <pre>s[lo:lo]</pre> <p> 是空的,而 <pre>s[lo:lo+1]</pre> <p> 有一个元素。 <pre class="source">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:]) }</pre> </div> <div class="slide"> <h2>Slice</h2> <p> slice 由函数 <code>make</code> 创建。这会分配一个零长度的数组并且返回一个 slice 指向这个数组: <pre>a := make([]int, 5) // len(a)=5</pre> slice 有长度和容量。slice 的容量是底层数组可以增长的最大长度。 <p> 为了指定容量,可传递第三个参数到 <code>make</code>: <p> <pre>b := make([]int, 0, 5) // len(b)=0, cap(b)=5</pre> slice 可以通过“重新切片”来扩容(增长到容量上限): <p> <pre>b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=4</pre> <pre class="source">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) }</pre> </div> <div class="slide"> <h2>Slice</h2> <p> slice 的零值是 <code>nil</code>。 <p> 一个 nil 的 slice 的长度和容量是 0。 <p> (了解更多关于 slice 的内容,参阅文章 “<a target="_blank" href="http://golang.org/doc/articles/slices_usage_and_internals.html">Slices:使用和内幕</a>”。) <pre class="source">package main import "fmt" func main() { var z []int fmt.Println(z, len(z), cap(z)) if z == nil { fmt.Println("nil!") } }</pre> </div> <div class="slide"> <h2>函数</h2> <p> 函数也是值。 <pre class="source">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)) }</pre> </div> <div class="slide"> <h2>函数</h2> <p> 并且函数是完全闭包的。 <p> 函数 <code>adder</code> 返回一个闭包。每个闭包被绑定到了特定的 <code>sum</code> 变量上。 <pre class="source">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), ) } }</pre> </div> <div class="slide"> <h2>Range</h2> <p> <code>for</code> 循环的 <code>range</code> 格式可以对 slice 或者 map 进行迭代循环。 <pre class="source">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) } }</pre> </div> <div class="slide"> <h2>Range</h2> <p> 可以将值赋值给 <code>_</code> 来忽略序号和值。 <p> 如果只需要索引值,去掉“<code>, value</code>”的部分即可。 <pre class="source">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) } }</pre> </div> <div class="slide"> <h2>Switch</h2> <p> 你可能已经猜到 <code>switch</code> 可能的形式了。 <p> case 体会自动终止,除非用 <code>fallthrough</code> 语句作为结尾。 <pre class="source">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) } }</pre> </div> <div class="slide"> <h2>Switch</h2> <p> switch 的条件从上到下的执行,当匹配成功的时候停止。 <p> (例如, <pre>switch i { case 0: case f(): }</pre> <p> 当 <code>i==0</code> 时不会调用 <code>f</code>。) <pre class="source">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.") } }</pre> </div> <div class="slide"> <h2>Switch</h2> <p> 没有条件的 switch 同 <code>switch true</code> 一样。 <p> 这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。 <pre class="source">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.") } }</pre> </div> <div class="slide"> <h2>练习:循环和函数</h2> <p> 作为练习函数和循环的简单途径,用牛顿法实现开方函数。 <p> 在这个例子中,牛顿法是通过选择一个初始点 <i>z</i> 然后重复这一过程求 <code>Sqrt(x)</code> 的近似值: <div style="text-align: center"> <img src="https://chart.googleapis.com/chart?cht=tx&chl=z=z-\frac{z^2-x}{2z}" alt="牛顿法"> </div> <p> 为了做到这个,只需要重复计算 10 次,并且观察不同的值(1,2,3,……)是如何逐步逼近结果的。 <p> 然后,修改循环条件,使得当值停止改变(或改变非常小)的时候退出循环。观察迭代次数是否变化。结果与 <a target="_blank" href="http://golang.org/pkg/math/#Sqrt">math.Sqrt</a> 接近吗? <p> 提示:定义并初始化一个浮点值,向其提供一个浮点语法或使用转换: <pre>z := float64(1) z := 1.0</pre> <pre class="source">package main import ( "fmt" ) func Sqrt(x float64) float64 { } func main() { fmt.Println(Sqrt(2)) }</pre> </div> <div class="slide"> <h2>练习:Map</h2> <p> 实现 <code>WordCount</code>。它应当返回一个含有 <code>s</code> 中每个 “word” 数量的 map。函数 <code>wc.Test</code> 针对这个函数执行一个测试用例,并打印成功或者失败。 <p> 你会发现 <a target="_blank" href="http://golang.org/pkg/strings/#Fields">strings.Fields</a> 很有帮助。 <pre class="source">package main import ( "<span class="appengineMode">tour</span><span class="localMode">bitbucket.org/mikespook/go-tour-zh</span>/wc" ) func WordCount(s string) map[string]int { return map[string]int{"x": 1} } func main() { wc.Test(WordCount) }</pre> </div> <div class="slide"> <h2>练习:Slice</h2> <p> 实现 <code>Pic</code>。它应当接受一个 slice 的长度 <code>dy</code>,和 slice 中每个元素的长度的 8 位无符号整数 <code>dx</code>。当执行这个程序,它会将整数转换为灰度(好吧,蓝度)图片进行展示。 <p> 图片的实现已经完成。可能用到的函数包括 <code>x^y</code>,<code>(x+y)/2</code> 和 <code>x*y</code>。 <p> (需要使用循环来分配 <code>[][]uint8</code> 中的每个 <code>[]uint8</code>。) <p> (使用 <code>uint8(intValue)</code> 在类型之间转换。) <pre class="source">package main import "<span class="appengineMode">tour</span><span class="localMode">bitbucket.org/mikespook/go-tour-zh</span>/pic" func Pic(dx, dy int) [][]uint8 { } func main() { pic.Show(Pic) }</pre> </div> <div class="slide"> <h2>练习:斐波纳契闭包</h2> <p> 现在来通过函数找些乐趣。 <p> 实现一个 <code>fibonacci</code> 函数,返回一个函数(一个闭包)可以返回连续的斐波纳契数。 <pre class="source">package main import "fmt" // fibonacci 函数会返回一个返回 int 的函数。 func fibonacci() func() int { } func main() { f := fibonacci() for i := 0; i < 10; i++ { fmt.Println(f()) } }</pre> </div> <div class="slide"> <h2>进阶练习:复数立方根</h2> <p> 让我们通过 <code>complex64</code> 和 <code>complex128</code> 来探索一下 Go 内建的复数。 对于立方根,牛顿法需要大量循环: <div style="text-align: center"> <img src="https://chart.googleapis.com/chart?cht=tx&chl=z=z-\frac{z^3-x}{3z^2}" alt="牛顿法"> </div> <p> 找到 2 的立方根,确保算法能够工作。在 <code>math/cmplx</code> 包中有 <a target="_blank" href="http://golang.org/pkg/math/cmplx/#Pow">Pow</a> 函数。 <pre class="source">package main import "fmt" func Cbrt(x complex128) complex128 { } func main() { fmt.Println(Cbrt(2)) }</pre> </div> <div class="toc">方法和接口</div> <div class="slide nocode"> <h2>方法和接口</h2> </div> <div class="slide"> <h2>方法</h2> <p> Go 没有类。然而,仍然可以在结构体类型上定义方法。 <p> <i>方法接收者</i>出现在 <code>func</code> 关键字和方法名之间的参数中。 <pre class="source">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()) }</pre> </div> <div class="slide"> <h2>方法</h2> <p> 事实上,可以对包中的<i>任意</i>类型定义任意方法,而不仅仅是结构体。 <p> 不能对来自其他包的类型或基础类型定义方法。 <pre class="source">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()) }</pre> </div> <div class="slide"> <h2>接收者为指针的方法</h2> <p> 方法可以与命名类型或命名类型的指针关联。 <p> 刚刚看到的两个 <code>Abs</code> 方法。一个是在 <code>*Vertex</code> 指针类型上,而另一个在 <code>MyFloat</code> 值类型上。 <p> 有两个原因需要使用指针接收者。首先避免在每个方法调用中拷贝值(如果值类型是大的结构体的话会更有效率)。其次,方法可以修改接收者指向的值。 <p> 尝试修改 <code>Abs</code> 的定义,同时 <code>Scale</code> 方法使用 <code>Vertex</code> 代替 <code>*Vertex</code> 作为接收者。 <p> 当 <code>v</code> 是 <code>Vertex</code> 的时候 <code>Scale</code> 方法没有任何作用。<code>Scale</code> 修改 <code>v</code>。当 <code>v</code> 是一个值(非指针),方法看到的是 <code>Vertex</code> 的副本,并且无法修改原始值。 <p> <code>Abs</code> 的工作方式是一样的。只不过,仅仅读取 <code>v</code>。所以读取的是原始值(通过指针)还是那个值的副本并没有关系。 <pre class="source">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()) }</pre> </div> <div class="slide"> <h2>接口</h2> <p> 接口类型是由一组方法定义的集合。 <p> 接口类型的值可以存放实现这些方法的任何值。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>接口</h2> <p> 类型通过实现那些方法来实现接口。 <p> <i>没有显式声明的必要。</i> <p> 隐式接口解藕了实现接口的包和定义接口的包:互不依赖。 <p> 因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。(对比其他语言) <p> <a target="_blank" href="http://golang.org/pkg/io/">包 io</a> 定义了 <code>Reader</code> 和 <code>Writer</code>;不一定要这么做。 <pre class="source">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") }</pre> </div> <div class="slide"> <h2>错误</h2> <p> 错误是可以用字符串描述自己的任何东西。 主要思路是由预定义的内建接口类型 <code>error</code>,和其返回返回字符串窜的方法 <code>Error</code> 构成。 <pre>type error interface { Error() string }</pre> <p> 当用 <code>fmt</code> 包的多种不同的打印函数输出一个 <code>error</code> 时,会自动的调用该方法。 <pre class="source">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) } }</pre> </div> <div class="slide"> <h2>Web 服务器</h2> <p> <a target="_blank" href="http://golang.org/pkg/net/http/">包 http</a> 通过任何实现了 <code>http.Handler</code> 的值来响应 HTTP 请求: <pre>package http type Handler interface { ServeHTTP(w ResponseWriter, r *Request) }</pre> <p> 在这个例子中,类型 <code>Hello</code> 实现了 <code>http.Handler</code>。 <p> <span class="localMode"> 访问 <a href="http://localhost:4000/" target="_blank">http://localhost:4000/</a> 会看到来自程序的问候。 </span> <span class="appengineMode"> <b>注意:</b> 这个例子无法在基于 web 的指南用户界面运行。为了尝试编写 web 服务器,可能需要<a target="_blank" href="http://golang.org/doc/install/">安装 Go</a>。 </span> <pre class="source">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) }</pre> </div> <div class="slide"> <h2>图片</h2> <p> <a target="_blank" href="http://golang.org/pkg/image/#Image">Package image</a> 定义了 <code>Image</code> 接口: <pre>package image type Image interface { ColorModel() color.Model Bounds() Rectangle At(x, y int) color.Color }</pre> <p> (参阅<a target="_blank" href="http://golang.org/pkg/image/#Image">文档</a>了解全部信息。) <p> 同样,<code>color.Color</code> 和 <code>color.Model</code> 也是接口,但是通常因为直接使用预定义的实现 <code>image.RGBAColor</code> 和 <code>image.RGBAColorModel</code> 而忽略它们。 <pre class="source">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()) }</pre> </div> <div class="slide"> <h2>练习:错误</h2> <p> 从之前的练习中复制 <code>Sqrt</code> 函数,并修改使其返回 <code>error</code> 值。 <p> <code>Sqrt</code> 接收到一个负数时,应当返回一个非 nil 的错误值。复数同样也不被支持。 <p> 创建一个新类型 <pre>type ErrNegativeSqrt float64</pre> <p> 使其成为一个 <code>error</code>,通过 <pre>func (e ErrNegativeSqrt) Error() string</pre> <p> 方法就可以让 <code>ErrNegativeSqrt(-2).Error()</code> 返回 <code>"cannot Sqrt negative number: -2"</code>。 <p> <b>注意:</b> 在 <code>Error</code> 方法内调用 <code>fmt.Print(e)</code> 将会让程序陷入死循环。 可以通过先转换 <code>e</code> 来避免这个: <code>fmt.Print(float64(e))</code>。为什么? <p> 修改 <code>Sqrt</code> 函数,使其接受一个负数时,返回 <code>ErrNegativeSqrt</code> 值。 <pre class="source">package main import ( "fmt" ) func Sqrt(f float64) (float64, error) { return 0, nil } func main() { fmt.Println(Sqrt(2)) fmt.Println(Sqrt(-2)) }</pre> </div> <div class="slide localMode"> <h2>练习:HTTP 处理</h2> <p> 实现下面的类型,并在其上定义 ServeHTTP 方法。 在 web 服务器中注册它们来处理指定的路径。 <pre>type String string type Struct struct { Greeting string Punct string Who string }</pre> <p> 例如,可以使用如下方式注册处理方法: <pre>http.Handle("/string", String("I'm a frayed knot.")) http.Handle("/struct", &Struct{"Hello", ":", "Gophers!"})</pre> <pre class="source">package main import ( "net/http" ) func main() { // your http.Handle calls here http.ListenAndServe("localhost:4000", nil) }</pre> </div> <div class="slide"> <h2>练习:图片</h2> <p> 还记得之前编写的图片生成器吗? 现在来编写另一个,不过这次将会返回一个 <code>image.Image</code> 的实现来代替 slice 的数据。 <p> 自定义的 <code>Image</code> 类型,要实现<a href="http://golang.org/pkg/image/#Image" target="_blank">必要的方法</a>,并且调用 <code>pic.ShowImage</code>。 <p> <code>Bounds</code> 应当返回一个 <code>image.Rectangle</code>,例如 <code>image.Rect(0, 0, w, h)</code>。 <p> <code>ColorModel</code> 应当返回 <code>image.RGBAColorModel</code>。 <p> <code>At</code> 应当返回一个颜色;在这个例子里,在最后一个图片生成器的值 <code>v</code> 匹配 <code>color.RGBAColor{v, v, 255, 255}</code>。 <pre class="source">package main import ( "image" "<span class="appengineMode">tour</span><span class="localMode">bitbucket.org/mikespook/go-tour-zh</span>/pic" ) type Image struct{} func main() { m := Image{} pic.ShowImage(m) }</pre> </div> <div class="slide"> <h2>练习:Rot13 读取器</h2> <p> 一般的模式是 <a target="_blank" href="http://golang.org/pkg/io/#Reader">io.Reader</a> 包裹另一个 <code>io.Reader</code>,用某些途径修改特定的流。 <p> 例如, <a target="_blank" href="http://golang.org/pkg/compress/gzip/#NewReader">gzip.NewReader</a> 函数输入一个 <code>io.Reader</code>(gzip 的数据流)并且返回一个同样实现了 <code>io.Reader</code> 的 <code>*gzip.Reader</code>(解压缩后的数据流)。 <p> 实现一个实现了 <code>io.Reader</code> 的 <code>rot13Reader</code>,用 <a target="_blank" href="http://en.wikipedia.org/wiki/ROT13">ROT13</a> 修改数据流中的所有的字母进行密文替换。 <p> <code>rot13Reader</code> 已经提供。通过实现其 <code>Read</code> 方法使得它匹配 <code>io.Reader</code>。 <pre class="source">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) }</pre> </div> <div class="toc">并发</div> <div class="slide nocode"> <h2>并发</h2> </div> <div class="slide"> <h2>Goroutine</h2> <p> <i>goroutine</i> 是由 Go 运行时环境管理的轻量级线程。 <pre>go f(x, y, z)</pre> <p> 开启一个新的 goroutine 执行 <pre>f(x, y, z)</pre> <p> <code>f</code>,<code>x</code>,<code>y</code> 和 <code>z</code> 是当前 goroutine 中定义的,但是在新的 goroutine 中运行 <code>f</code>。 <p> goroutine 在相同的地址空间中运行,因此访问共享内存必须进行同步。 <code><a href="http://golang.org/pkg/sync/" target="_blank">sync</a></code> 提供了这种可能,不过在 Go 中并不经常用到,因为有其他的办法。 (在接下来的内容中会涉及到。) <pre class="source">package main import ( "fmt" "<span class="appengineMode">runtime</span><span class="localMode">time</span>" ) func say(s string) { for i := 0; i < 5; i++ { <span class="appengineMode">runtime.Gosched()</span><span class="localMode">time.Sleep(100 * time.Millisecond)</span> fmt.Println(s) } } func main() { go say("world") say("hello") }</pre> </div> <div class="slide"> <h2>Channel</h2> <p> channel 是有类型的管道,可以用 channel 操作符 <code><-</code> 对其发送或者接收值。 <pre>ch <- v // 将 v 送入 channel ch。 v := <-ch // 从 ch 接收,并且赋值给 v。</pre> <p> (“箭头”就是数据流的方向。) <p> 和 map 与 slice 一样,channel 使用前必须创建: <pre>ch := make(chan int)</pre> <p> 默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>缓冲 channel</h2> <p> channel 可以是<i>带缓冲的</i>。为 <code>make</code> 提供第二个参数作为缓冲长度来初始化一个缓冲 channel: <pre>ch := make(chan int, 100)</pre> <p> 向缓冲 channel 发送数据的时候,只有在缓冲区满的时候才会阻塞。当缓冲区清空的时候接受阻塞。 <p> 修改例子使得缓冲区被填满,然后看看会发生什么。 <pre class="source">package main import "fmt" func main() { c := make(chan int, 2) c <- 1 c <- 2 fmt.Println(<-c) fmt.Println(<-c) }</pre> </div> <div class="slide"> <h2>Range 和 Close</h2> <p> 发送者可以 <code>close</code> 一个 channel 来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试 channel 是否被关闭:当没有值可以接收并且 channel 已经被关闭,那么 <pre>v, ok := <-ch</pre> <p> <code>ok</code> 会被设置为 <code>false</code>。 <p> 循环 <code>for i := range c</code> 会不断从 channel 接收值,直到它被关闭。 <p> <b>注意:</b> 只有发送者才能关闭 channel,而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。 <p> <b>还要注意</b>:channel 与文件不同;通常情况下无需关闭它们。只有在需要告诉接收者没有更多的数据的时候才有必要进行关闭,例如中断一个 <code>range</code>。 <pre class="source">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) } }</pre> </div> <div class="slide"> <h2>Select</h2> <p> <code>select</code> 语句使得一个 goroutine 在多个通讯操作上等待。 <p> <code>select</code> 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。 <pre class="source">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) }</pre> </div> <div class="slide"> <h2>默认选择</h2> <p> 当 <code>select</code> 中的其他条件分支都没有准备好的时候,<code>default</code> 分支会被执行。 <p> 为了非阻塞的发送或者接收,可使用 <code>default</code> 分支: <pre>select { case i := <-c: // use i default: // receiving from c would block }</pre> <p> <span class="appengineMode"> <b>注意:</b> 这个例子无法在指南的基于 web 的用户界面中运行,因为沙盒环境是没有定时器概念的。需要<a target="_blank" href="http://golang.org/doc/install/">安装、 Go</a> 来了解这个例子如何运行。 </span> <pre class="source">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) } } }</pre> </div> <div class="slide nocode"> <h2>练习:等价二叉树</h2> <p> 可以用多种不同的二叉树的叶子节点存储相同的数列值。例如,这里有两个二叉树保存了序列 1,1,2,3,5,8,13。 <img src="static/fig4.png" alt="二叉树"> <p> 用于检查两个二叉树是否存储了相同的序列的函数在多数语言中都是相当复杂的。这里将使用 Go 的并发和 channel 来编写一个简单的解法。 <p> 这个例子使用了 <code>tree</code> 包,定义了类型: <pre>type Tree struct { Left *Tree Value int Right *Tree }</pre> </div> <div class="slide"> <h2>练习:等价二叉树</h2> <p> <b>1.</b> 实现 <code>Walk</code> 函数。 <p> <b>2.</b> 测试 <code>Walk</code> 函数。 <p> 函数 <code>tree.New(k)</code> 构造了一个随机结构的二叉树,保存了值 <code>k</code>,<code>2k</code>,<code>3k</code>,...,<code>10k</code>。 <p> 创建一个新的 channel <code>ch</code> 并且对其进行步进: <pre>go Walk(tree.New(1), ch)</pre> <p> 然后从 channel 中读取并且打印 10 个值。应当是值 1,2,3,...,10。 <p> <b>3.</b> 用 <code>Walk</code> 实现 <code>Same</code> 函数来检测是否 <code>t1</code> 和 <code>t2</code> 存储了相同的值。 <p> <b>4.</b> 测试 <code>Same</code> 函数。 <p> <code>Same(tree.New(1), tree.New(1))</code> 应当返回 true,而 <code>Same(tree.New(1), tree.New(2))</code> 应当返回 false。 <pre class="source">package main import "<span class="appengineMode">tour</span><span class="localMode">bitbucket.org/mikespook/go-tour-zh</span>/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() { }</pre> </div> <div class="slide"> <h2>练习:Web 爬虫</h2> <p> 在这个练习中,将会使用 Go 的并发特性来并行执行 web 爬虫。 <p> 修改 <code>Crawl</code> 函数来并行的抓取 URLs,并且保证不重复。 <pre class="source">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/", }, }, }</pre> </div> <div class="slide nocode"> <h2>Where to Go from here...</h2> <p class="appengineMode"> 可以从 <a href="http://golang.org/doc/install/">installing Go</a> 或者下载 <a href="http://code.google.com/appengine/downloads.html#Google_App_Engine_SDK_for_Go">Go App Engine SDK</a>。 </p> <p> <span class="appengineMode">一旦在机器上安装好了 Go,</span> <span class="localMode"></span> <a target="_blank" href="http://golang.org/doc/">Go Documentation</a> <span class="appengineMode">是应当继续阅读的内容</span> <span class="localMode">作为开始</span>。 它包含了参考、指南、视频等等更多资料。 <p> 在标准库上需要帮助的话,参考 <a target="_blank" href="http://golang.org/pkg/">package reference</a>。语言本身的帮助,可以阅读令人愉快的 <a target="_blank" href="http://golang.org/ref/spec">Language Spec</a>。 <p> 进一步探索 Go 的并发模型,参阅 <a target="_blank" href="http://golang.org/doc/codewalk/sharemem/">Share Memory by Communicating</a> 的代码之旅。 <p> <a target="_blank" href="http://golang.org/doc/codewalk/functions/">First Class Functions in Go</a> 为 Go 的函数类型提供了有趣的展示。 <p> <a target="_blank" href="http://blog.golang.org/">Go Blog</a> 有着众多的关于 Go 的文章信息。 <p> 访问 <a target="_blank" href="http://golang.org">golang.org</a> 了解更多内容。 </div> </div><!-- end slides --> <div id="workspace"> <div class="controls"> <div><a id="run" href="#run" title="编译并运行">运行</a><a href="#more" id="more" title="选项">▼</a></div> <ul class="more"> <li><a href="#" id="reset">重置幻灯</a></li> <li><a href="#" id="format">格式化代码</a></li> <li><a href="#" id="kill" class="localMode">终止程序</a></li> <li><hr></li> <li><a href="#" id="togglesyntax">语法高亮:关闭</a></li> <li><a href="#" id="togglelineno">行号:开启</a></li> </ul> </div> <div id="workspace-top"> <div id="workspace-editor"> <textarea id="editor" spellcheck="false"></textarea> </div> </div> <div id="workspace-bottom"> <div id="output"></div> </div> </div> </div> </body> </html>