如果你已经对我以前的辅导教程都有过学习或者有这样的知识,那么你应该可以开始基于Go语言的Web编程了。

网络服务总是在等待用户电脑的连接、请求,并根据用户的请求进行一些处理之后返回用户需要的数据给用户,用户通过URL地址访问网络服务,每一个URL都是唯一的,它代表着网络上的某一个资源,比如你现在看到本文,它的URL地址就对应着这篇文章,不会是别的。

我们的Go语言Web编程的Hello World程序,将运行在本地电脑上,并且会监听9999端口,也就是说,当你打开浏览器,在地址栏里面输入 http://localhost:9999 的时候,就可以访问到我们的这个程序。

package main

import (
    "net/http"
    "fmt"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Inside hanler")
    fmt.Fprintf(w, "Hello World from my Go Program!")
}

func main() {
    http.HandleFunc("/", handler) // 生定向所有URL到 handler 函数
    http.ListenAndServe("localhost:9999", nil) // 监听来自9999端口的连接请求
}

运行它之后,程序将不会有任何显示,但是你可以打开浏览器,在地址栏中输入http://localhost:9999,可以在网页中看到“Hello World from my Go Program!”,这表示你的Web程序已经开始服务了,而这个时候你返回去看运行它的终端,每一次对网页的访问,都会打印出几个“Inside hanler”字符。

现在让我们来分析一下下我们在代码都具体做了什么:

  • 像任何其它的Go程序一样,要想让程序可以执行,那么必须有一个 main 函数
  • 为了在终端打印一些信息,所以我们像其它的示例一样,导入了fmt 包
  • 我们导入了与Web http协议相关的包 http*,所有与该包相关的函数我们都以 *http.FuncName的形式调用。
  • 在 main 函数中,我们将所有的请求都转发给 handler 函数,接着我们呼叫 http.HandleFunc 方法,并且传递了两人个参数,第一个参数为请求的URL地址,第二个为处理该URL请求的函数。
  • 我们使用 http.ListenAndServer() 方法打开服务程序,并且监听了 localhost:9999
  • 当我们打开浏览器访问 *http://localhost:9999*的时候,程序返回了“Hello World from my Go Program!”字符串。
  • 从 http.Request 你可以获取关于当次请求的所有细节,比如URL地址,或者用户输入的数据等。
  • 你应该使用接收到的 http.ResponseWriter 指针来发送需要返回给客户的内容。
  • 代码 *http.HandleFunc(“/”, handler) 表示所有针对该地址的请求都将转发给 *handler*函数

现在让我们对上面的示例程序稍稍地扩展一下,让它可以接收一些用户的数据,然后将用户输入的数据展示给用户:

package main

import (
    "net/http"
    "fmt"
    "strings"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    remPartOfURL := r.URL.Path[len("/hello/"):] //获取 URL 地址中 /hello/ 部分之后的所有内容
    fmt.Fprintf(w, "Hello %s", remPartOfURL)
}

func shouthelloHandler(w http.ResponseWriter, r *http.Request) {
    remPartOfURL := r.URL.Path[len("/shouthello/"):] // 同上
    fmt.Fprintf(w, "Hello %s!", strings.ToUpper(remPartOfURL))
}

func main() {
    http.HandleFunc("/hello/", helloHandler)
    http.HandleFunc("/shouthello/", shouthelloHandler)
    http.ListenAndServe("localhost:9999", nil)
}

现在,运行该程序,然后访问下面这些地址看看:

本文是Go 中的 Channels的下半部分,如果你还没有阅读过第一部分,建议你先阅读它。

这里我们还有一个问题,那就是数据的接收者无法知道什么时候应该停止等待数据,是否还有更多新数据或者所有数据都已经接收到了?我们应该继续等待还是可以进行下一步的工作了?一种解决办法是不断的循环查询数据来源是否已经关闭,如果Channel已经关闭,则知道数据已经全部接收完成了,但是这并不十分有效。Go提供了一个关键字 *range*,它可以帮助我们一直监听Channel直到它关闭。

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string, count int) {
    for i := 1; i <= count; i++ {
        cakeName := "Strawberry Cake " + strconv.Itoa(i)
        cs <- cakeName // 传递一个 cake
    }
}

func receiveCakeAndPack(cs chan string) {
    for s := range cs {
        fmt.Println("Packing received cake: ", s)
    }
}

func main() {
    cs := make(chan string)
    go makeCakeAndSend(cs, 5)
    go receiveCakeAndPack(cs)

    du,_ := time.ParseDuration("3s")
    time.Sleep(du)
}

输出为:

Packing received cake:  Strawberry Cake 1
Packing received cake:  Strawberry Cake 2
Packing received cake:  Strawberry Cake 3
Packing received cake:  Strawberry Cake 4
Packing received cake:  Strawberry Cake 5

对于 makeCakeAndSend*来说,我们已经指定了要制作的蛋糕数,但是对于 *receiveCakeAndPack*来说,它事先并不知道将有多少个蛋糕需要它打包,在上一个示例中,我们将这个蛋糕数硬编码至代码中,所以,它知道什么时候完成,但是现在我们改用了 *range 这个关键字,现在当 channel 关闭之后,循环会自动退出。

Channel 与 select

我们还有一种情况,那就是多台生产蛋糕的电脑可以通过多个传送带向同一个打包的电脑传送蛋糕,因为理论上讲蛋糕生产速度是没有打包速度快的,所以,我现在两人台电脑同时生产蛋糕,然后配备一台电脑用来打包,这个时候我们可以使用一个传送带(一个Channel):

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string, flavor string, count int) {
    for i := 1; i <= count; i++ {
        cakeName := flavor + " Cake " + strconv.Itoa(i)
        cs <- cakeName // 传递一个 cake
    }
}

func receiveCakeAndPack(cs chan string) {
    for s := range cs {
        fmt.Println("Packing received cake: ", s)
    }
}

func main() {
    cs := make(chan string)
    go makeCakeAndSend(cs, "Strawberry", 5)
    go makeCakeAndSend(cs, "Chocolate", 5)
    go receiveCakeAndPack(cs)

    du,_ := time.ParseDuration("2s")
    time.Sleep(du)
}

运行结果为:

Packing received cake:  Strawberry Cake 1
Packing received cake:  Chocolate Cake 1
Packing received cake:  Strawberry Cake 2
Packing received cake:  Chocolate Cake 2
Packing received cake:  Strawberry Cake 3
Packing received cake:  Chocolate Cake 3
Packing received cake:  Strawberry Cake 4
Packing received cake:  Chocolate Cake 4
Packing received cake:  Strawberry Cake 5
Packing received cake:  Chocolate Cake 5

但是我们还可以使用一个 select 关键字来以另一种方式选择,我们为了提供生产效率,除了采用两人台电脑生产蛋糕之外,我们还使用两个传送带来传送,这样就不会因为传送带一次只能传递一个蛋糕而影响效率了:

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string, flavor string, count int) {
    for i := 1; i <= count; i++ {
        cakeName := flavor + " Cake " + strconv.Itoa(i)
        cs <- cakeName
    }
}

func receiveCakeAndPack(strbry_cs chan string, choco_cs chan string) {
    strbry_closed, choco_closed := false, false
    for {
        if (strbry_closed && choco_closed) { return }
        fmt.Println("Waiting for a new cake ...")
        select {
        case cakeName, strbry_ok := <- strbry_cs:
            if (!strbry_ok) {
                strbry_closed = true
                fmt.Println("... Strawberry channel closed!")
            } else {
                fmt.Println("Received from Strawberry channel. Now packing ", cakeName)
            }
        case cakeName, choco_ok := <- choco_cs:
            if (!choco_ok) {
                choco_closed = true
                fmt.Println("... Chocolate channel closed!")
            } else {
                fmt.Println("Received from Chocolate channel. Now packing ", cakeName)
            }
        }
    }
}

func main() {
    scs := make(chan string)
    ccs := make(chan string)
    go makeCakeAndSend(scs, "Strawberry", 3)
    go makeCakeAndSend(ccs, "Chocolate", 3)
    go receiveCakeAndPack(scs,ccs)
    du, _ := time.ParseDuration("3s")
    time.Sleep(du)
}

输出结果为:

Waiting for a new cake ...
Received from Chocolate channel. Now packing  Chocolate Cake 1
Waiting for a new cake ...
Received from Chocolate channel. Now packing  Chocolate Cake 2
Waiting for a new cake ...
Received from Strawberry channel. Now packing  Strawberry Cake 1
Waiting for a new cake ...
Received from Chocolate channel. Now packing  Chocolate Cake 3
Waiting for a new cake ...
Received from Strawberry channel. Now packing  Strawberry Cake 2
Waiting for a new cake ...
Received from Strawberry channel. Now packing  Strawberry Cake 3
Waiting for a new cake ...

Goroutines 允许你以并行的方式运行程序,但是要让这种并行有实际的用途,我们还有一些其它的需求——我们应该能够向运行中的某一个进程传递数据,并且也应该可以从一个运行中的进程中获取数据,这就需要使用到Go的Channels了。

一个Channel可以被想象成为一个定义了大小和能力的管道或传送带,我们可以将数据从它的一侧放入,然后在另一侧取出。我们拿蛋糕工厂中的传送带来打比方的话,Channel就是那个传送带,有一台电脑负责制作蛋糕,它将做好的蛋糕放到传送带上面来,然后另一台负责打包的电脑就在等待传送带上面的蛋糕,当它等到一个制作好的蛋糕之后就将其从传送带上面取出然后打包进行下一个处理,在这个过程中,传送带(也就是我们的Channel)起到了让负责制作和打包的两台电脑通讯的功能。

传送带就可以被称之为Channel
+++++++++++++++++++++++++++++++++ +   打好包的蛋
制作电脑 -> -> 蛋糕 -> -> 蛋糕 -> -> 蛋糕 -> 打包电脑 -> ->  ++ 糕,可以进
+++++++++++++++++++++++++++++++++ +   行其它处理

在Go中,关键字 chan 被用来定义Channel,make 关键字则被用来创建它,并且定义它能传送的数据类型(是传送蛋糕的传送带而不是传送鸡蛋的)。

ic := make(chan int)
sc := make(chan string)
mc := make(chan mytype)

你可以使用 <- 操作符来向Channel传送或者取回数据,比如:

ic := make(chan int)
//在某些Goroutines中向其传送数据:
ic <- 5
//在另一些Goroutines中取出其中的数据
var recv int
recv = <- ic

你还可以使用 <- 操作符定义Channel中数据的移动方向:

sendOnlyChan := make( <- chan int) //只能发送数据
recvOnlyChan := make( chan -< int) //只能接收数据

一个Channel可以容纳数据的容量是十分分重要,它表示了多少个项目可以被同时存储在Channel中,就比如制作蛋糕的电脑一分钟可以制作十个蛋糕,但是打包的电脑却只能一分钟打包五个,这样就会有太多的蛋糕因为无法来得急打包而制作电脑又源源不断的传送带上面放新制作好的蛋糕而被丢掉浪费,这在计算机并行运算中,就是所谓的生产者-消费者同步问题(当然了,Channel是不移动的)。

如果一个Channel的容量是 1,那么当一份数据被发送到该Channel之后,就必须要等待这份数据被取出之后,下一份数据才能再被发送进来,发送者与接收者每一次只能在一个时间传送一份数据,而且任何一方都必须要等待另一方执行完成之后才能传送或者接收下一个份数据。我们的示例将从同步Channel开始。

我们到现在为止所定义的任何一个Channel都是同步的——新数据必须在旧数据被取走后才能传送进去,现在让我们来看看这个蛋糕制作工厂在Go是如何实现的:

package main

import (
    "fmt"
    "time"
    "strconv"
)

var i int

func makeCakeAndSend(cs chan string) {
    i = i + 1
    cakeName := "Strawberry Cake " + strconv.Itoa(i)
    fmt.Println("Making a cake and sending...", cakeName)
    cs <- cakeName // send a strawberry cake
}

func receiveCakeAndPack(cs chan string) {
    s := <- cs // get whatever cake is on the channel
    fmt.Println("Packing received cake: ", s)
}

func main() {
    cs := make(chan string)
    for i := 1; i < 3; i++ {
        go makeCakeAndSend(cs)
        go receiveCakeAndPack(cs)
    }
    // Sleep for a while so that the program doesn't exit immediately and   output is clear for illustration
    wait, _ := time.ParseDuration("2s")
    time.Sleep(wait)
}

输出结果为:

Making a cake and sending ... Strawberry Cake 1
Packing received cake: Strawberry Cake 1
Making a cake and sending ... Strawberry Cake 2
Packing received cake: Strawberry Cake 2
Making a cake and sending ... Strawberry Cake 3
Packing received cake: Strawberry Cake 3

上面的代码中,我们进行了三次请求来制作一个蛋糕,并立马将其打包,。因为我们的生产方(makeCakeAndSend)与消费方(receiveCakeAndPack)是同步的,所以在生产方生产好蛋糕之后必须等待消费方接收数据,它才能再一次往Channel中发送数据。

现在我们来改变一下下代码,通常的,Goroutines是一个在一直重复着运行的代码块,它们对数据进行操作并且通过Channels与其它的Goroutines进行数据的传送,在下一个示例中,我们将上面示例的循环移动到Goroutine里面去,这样我们只请求Goroutine一次:

package main

import (
    "fmt"
    "time"
    "strconv"
)

func makeCakeAndSend(cs chan string) {
    for i := 1; i<=3; i++ {
        cakeName := "Strawberry Cake " + strconv.Itoa(i)
        fmt.Println("Making a cake and sending...", cakeName)
        cs <- cakeName
    }
}

func receiveCakeAndPack(cs chan string) {
    for i := 1; i<=3; i++ {
        s := <- cs
        fmt.Println("Packing received cake: ", s)
    }
}

func main() {
    cs := make(chan string)
    go makeCakeAndSend(cs)
    go receiveCakeAndPack(cs)

    wait, _ := time.ParseDuration("5s")
    time.Sleep(wait)
}

输出为:

Making a cake and sending... Strawberry Cake 1
Making a cake and sending... Strawberry Cake 2
Packing received cake:  Strawberry Cake 1
Packing received cake:  Strawberry Cake 2
Making a cake and sending... Strawberry Cake 3
Packing received cake:  Strawberry Cake 3

上面的输出只是在我电脑上面的,但是你的并不一定也是这样一模一样的输出,由于我们将循环移到了Goroutines里面,所以对于 makeCakeAndSend 和 receiveCakeAndPack 会分别制作三个蛋糕并传送到Channel上和取出三个蛋糕并打包。

Goroutines 允许您能够并行的执行任务——为什么又出来了这么一个新词儿?虽然现在已经有很多形容并行的词了,比如线程、协程、进程等等,但是没有一个已有的词汇能精确地表达出Goroutines的内涵,一个Goroutines就是一个与其它的Goroutine在同一地址空间中并行执行的函数或者方法,一个运行中的程序有一个或者多个Goroutines组成,Go官方文档如下面这么说道:

Goroutines被复用到多个系统线程上,所以,如果某一个Goroutine阻塞了,并不会影响其它的Goroutine。 Goroutines为我们隐藏了很多的内部的并行机制,这样的设计让语言的设计者可以根据需要修改它底层并行的实现,以改进其运行性能或者进行硬件优化,而作为语言使用者的我们则不需要修改任何一个代码,就能获得性能的提升。

在大多数程序中,我们都是按顺序执行的,因为作为用户的人的处理能力是远远不及计算机的,比如当我们在写代码的时候,我们敲击键盘的速度比计算机处理敲击的速度是慢得多的,所以,计算机在这种情况下一直是处于等待我们的输入的状态。但是对于一台每一秒都处理成千上万请求的服务器来说,这种顺序执行的方法显然是不可能的,如果一百个人同时请求,我们不可能按顺序的执行每一个用户的请求,这个时候就需要并发,在Go中,我们使用Goroutines。

Goroutines与普通的函数没有什么不一样的,只是它执行它的时候,需要在函数的名称前面使用一个 go 关键字,如果我们 myFunc() 是一个方法,那么如果要让它以一个Goroutine的方式执行,只需要 go myFunc() 即可——这将使得它以一个新的线程运行,让我们来做一个简单的示例:

package main
import "fmt"
func LoopIt(times int) {
    for i := 0; i < times; i++ {
        fmt.Printf("%dt", i)
    }
    fmt.Println("--Loop End--")
}
func main() {
    LoopIt(10)
    LoopIt(10)
}

输出为:

0   1   2   3   4   5   6   7   8   9   --Loop End--
0   1   2   3   4   5   6   7   8   9   --Loop End--

然后我们对程序做一个很小的改动:

//...
func main() {
    go LoopIt(10)
    LoopIt(10)
}

它的输出结果可能就成为:

0   0   1   2   1   3   2   4   3   5   4   6   5   7   6   8   7   9   8   --Loop End--
--Loop End--

为什么会有这样的区别?因为当我启动一个 Goroutine 之后,LoopIt这个函数立马就会执行,但是它并不会阻塞整个程序,所以,它开始执行之后,它后面的函数也会开始执行,后面的函数就是一个普通的函数,所以它会顺序执行,这使得两人个函数同时运行了,所以我们上面的输出中,这样一来,LoopIt 会一直输出数字,而由于go LoopIt也在执行,它也会输出数据,所以就出现了上面那种情况,这两人个 LoopIt并行了。

再来看看下面这个例子:

package main
import (
    "fmt"
    "time"
)
func simulateEvent(name string, timeInSecs string) {
    fmt.Println("Started ", name, ": Should take", timeInSecs, "seconds")
    du, _ := time.ParseDuration(timeInSecs)
    time.Sleep(du)
    fmt.Println("Finished ", name)
}
func main() {
    simulateEvent("First", "10s")
    simulateEvent("Second", "6s")
    simulateEvent("Third", "3s")
}

输出结果为:

Started  First : Should take 10s seconds
Finished  First
Started  Second : Should take 6s seconds
Finished  Second
Started  Third : Should take 3s seconds
Finished  Third

这是一个顺序执行的程序,所以,我们可以看到,必须是第一个函数执行完了之后才执行第二个函数,但是我们再来看看下面这个示例:

package main
import (
    "fmt"
    "time"
)
func simulateEvent(name string, timeInSecs string) {
    fmt.Println("Started ", name, ": Should take", timeInSecs, "seconds")
    du, _ := time.ParseDuration(timeInSecs)
    time.Sleep(du)
    fmt.Println("Finished ", name)
}
func main() {
    go simulateEvent("First", "10s")
    go simulateEvent("Second", "6s")
    go simulateEvent("Third", "3s")
    wait, _ := time.ParseDuration("15s")
    time.Sleep(wait)
    fmt.Println("Exited")
}

输出为:

Started  First : Should take 10s seconds
Started  Second : Should take 6s seconds
Started  Third : Should take 3s seconds
Finished  Third
Finished  Second
Finished  First
Exited

所有的函数都同时运行了,然后因为第三个函数只暂时3秒,所以它最先运行完成,而第二个第二完成,第一个因为要暂时10秒,所以最后完成,我们在 main() 函数中使用暂时了程序15秒,以让上面的三个 Goroutines运行完成之后才退出程序。

多态,按字面意思来讲就是多种状态,在面向对象编程语言中,接口的多种不同的实现方式即为多态。引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘自“Delphi4 编程技术内幕”)。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal和C++中都是通过虚函数(Virtual Function) 实现的。

还是用简单的例子来讲明什么要多态吧,我们有一个名为“人”的接口,它有一个方法“我喜欢”,然后一个从火星来的朋友就是程序的使用者,它来到地球之后先问了一个男人:“你喜欢做什么?”,男人调用“我喜欢”这个方法返回给他一个值:“钓鱼”;然后火星人又去问了一个女人同样的问题:“你喜欢做什么?”,然后女人也实现了“人”这个接口,她也调用“我喜欢”这个方法返回给火星人一个值:“购物”……如此这么问下去,发现,每一个人对:“你喜欢做什么?”这个问题的问题都是不一样的,而这种不一样,就是*多态*——从同一种类型的同一个方法中获取完全不同的结果——在这个例子中,接接口就是“人”。

Go中,我们使用接口来实现多态,如果你看这篇文章时,还不了解Go的接口,建议你先看看我的另一篇文章Go 中的接口,然后再返回来看本文。创建我们创建了一个接口,并且有其它的类型实现了这个接口,那么我们就可以通过这个接口直接访问到每一个类型中所定义的那个方法,而不需要再知道它具体是什么类型,让我们用Go来实现上面那个关于火星人问问题的例子:

package main
import "fmt"
// 定义人这个接口
type Human interface {
    // 它有一个方法 iLove
    iLove() string
}
// 定义男人
type Man struct {}
// 实现人这个接口
func (man Man) iLove() string {
    return "钓鱼"
}
//重复上面的定义
type Woman struct {}
func (woman Woman) iLove() string {
    return "购物"
}
func main() {
    m := new (Man)
    w := new (Woman)
    // 一个保存人的数组,它有两人个元素
    // 一个为 Man 类型,一个为Woman类型
    humans := [...]Human{m,w}
    for i, _ := range (humans) {
        // 直接调用接口的 iLove 方法
        fmt.Println("我喜欢", humans[i].iLove())
    }
}

输出为:

我喜欢 钓鱼
我喜欢 购物

在上面的例子了,我们都是调用了 Human 的 iLove 方法,但是得到了两人个结果,而且我们是直接使用 for 循环来对保存了 Human 数据的数组进行遍历而不是明显地使用 Man.iLove() 方法或者Woman.iLove() 方法。

现在又有一个新问题产生了,这火星人并不知道在地球上我们把人和其它生物是作不同对待的,所以,他还跑去问了一只狗:“你喜欢做什么啊”?狗很客气的“汪!汪汪!汪汪汪!”,那这种新情况出现了我们该怎么办?好办,让我们再来扩展一下上面的例子:

package main
import "fmt"
// 定义生物这个接口
type Organism interface {
    // 它有一个方法 iLove
    iLove() string
}
// 定义人这种生物
type Human struct {}
// 定义人对生物这个接口的实现
func (human Human) iLove() string {
    return "做人该做的事情,我是一只人,不是一个狗!"
}
// 定义男人
type Man struct { Human }
func (man Man) iLove() string {
    return "钓鱼,我是一只男人。"
}
//重复上面的定义
type Woman struct { Human }
func (woman Woman) iLove() string {
    return "购物,我是一只女人。"
}
// 定义狗
type Dog struct {}
func (dog Dog) iLove() string {
    return "汪!汪汪!汪汪汪!我是一个狗!不是一只人。"
}
func main() {
    h := new (Human)
    m := new (Man)
    w := new (Woman)
    d := new (Dog)
    organisms := [...]Organism{h, m, w, d}
    for i, _ := range (organisms) {
        // 直接调用接口的 iLove 方法
        fmt.Println("我喜欢", organisms[i].iLove())
    }
}

输出为:

我喜欢 做人该做的事情,我是一只人,不是一个狗!
我喜欢 钓鱼,我是一只男人。
我喜欢 购物,我是一只女人。
我喜欢 汪!汪汪!汪汪汪!我是一个狗!不是一只人。

在我那篇《Go 中的接口》的文章中,我用了一个很简单的示例来讲明Go的接口实现——矩形与方形同时实现了形状接口。但是我们要清楚的认识到,Go的接口并不是一种C或者Java的变体,而是具有了很多的特性,它是大规模程序开发及高可用性程序开发所需要的关键。

先看看Java里面接口的实现方法,然后再返回来看看Go的接口的实现方式,我们就能很容易发现为什么说Go的接口实现可用性可扩展性更高,而且进化到另一个层次。我们的数据模型是 *Bus*,它同时实现了两人个接口——只有可用空间的立方体以及可以运输乘客的运输工具:

// 第一步:在编码之前,先设计好接口以及类的继承
// 第二步:将现实世界中的对象抽象成为我们可以在程序中使用的接口
interface PublicTransport {
    int PassengerCapacity();
}
interface Cuboid {
    int CubicVolumn();
}
// 第三步:创建数据结构同时去实现我们在第二步中定义的所有接口
public class Bus implements
PublicTransport,
Cuboid {
    // 定义类的数据结构
    int l, b, h, rows, seatsPerRow;
    public Bus(int l, int b, int h, int rows, int seatsPerRow) {
        this.l =l; this.b = b; this.h = h; this.rows = rows;
        this.seatsPerRow = seatsPerRow;
    }
    // 定义方法的实现
    public int CubicVolumn() { return l*b*h; }
    public int PassengerCapacity() { return rows * seatsPerRow; }
    // 在 main 函数中调用类及其方法
    public static void main() {
        Bus b = new Bus(10, 6, 3, 10, 5);
        System.out.Println(b.CubicVolume());
        System.out.Println(b.PassengerCapacity());
    }
}

在上面的Java示例中,你必须在实现接口方法之前明确地定义类的数据层次结构,并且需要使用implements 这个关键字,C# 中则是将接口名称在 : 符号后面列出来代替 implements 关键字,在这种情况下,任何这部分的需求的变更都会影响到项目的核心模块。

绝大数的项目就是在这种先定义接口然后去实现最后发现不能完全需求,又返回去修改接口中再实现再修改,如此往复,直到接口已经考虑到各方面的因素/涵盖了所有需求——而如果此时项目的需求还在变化的话,那工作量就更大了。在完全确定了接口之后,就不再推荐修改这些最底层的接口了,即使是项目管理员也不允许随意的修改它,而确实需要修改的话,有可能还需要通过各种各样大大小小的项目组研讨会等最后才能确定是否修改以及如何修改,因为接口做为最底层,它的一点修改,都会影响到上层的实现,如果一个接口有一百个实现,那么它的修改将需要对其一百个实现也做相应的修改。或者还有另一种方法,那就是重新创建一个新的接口,接口的定义就是现在这个接口修改后的定义,然后再创建新的类去实现新的接口。当我们的项目达到某一个层次之后,发现所有的这些事情已经完全失控了。

那有没有更好的办法?我们先来看看上面这个Java示例的的Go的实现:

package main
import "fmt"
// Go第一步:定义你的数据结构
type Bus struct {
    l, b, h int
    rows, seatsPerRow int
}
// Go第二步:定义一个现实世界对象的抽象层
type Cuboider interface {
    CubicVolume() int
}
// Go第三步:实现接口的方法
func (bus Bus) CubicVolume() int {
    return bus.l * bus.b * bus.h
}
// 重复上面的二三步
type PublicTransporter interface {
    PassengerCapacity() int
}
func (bus Bus) PassengerCapacity() int {
    return bus.rows * bus.seatsPerRow
}
func main() {
    bus := Bus{
        l:10, b:6, h:3,
        rows: 10, seatsPerRow: 5}
    fmt.Println("Cubic volume of bus: ", bus.CubicVolume())
    fmt.Println("Maximum number of passengers:", bus.PassengerCapacity())
}

在上在面的Go的实现中,所使用的名词与Java版本都一样,但是这之间却还是有很多的不一样,比如最直接的区别是我们看不到 implements 这种在Java中用来定义类的接口实现的关键字了。另外,Go中,看起来似乎是以数据为中心的,你先定义了你的数据结构,然后定义它的接口,而这种接口与实现之间的层次结构是没有明确说明的,我们没有使用特别的签名来关联接口与实现,而是Go根据语法规则自动的为你实现了接口。在这个示例中,似乎我们看不到Go的实现方法有任何的好处,但是当项目越来越大,或者需求变更时,Go的方法将被证明是更好的。

让我们假设,随着时间的发展,一个项目要求我们的巴士——法律要求,每一个乘客都应该有至于自己的最低限度的立体空间,因为我们之前只是按座位计算可乘客人数,而现还需要考虑汽车的空间以及乘客所能分配到的平均空间量。

那么,我们的汽车需要实现一个新的接口 PersonalSpaceLaw ,它的方法在任何其它的接口都是没有被实现过的(我们不得不这么做,以适应新的法律要求):

// 新的需求
interface PersonalSpaceLaw {
    boolean IsCampliantWithLaw();
}

class Bus implements
PublicTransport,
Cuboid,
PersonalSpaceLaw {
    //.....
    public IsCampliantWithLaw() {
        return (l * b * h ) / ( rows * seatsPerRow ) > 3;
    }
    //......
}

该需要的改变需要我们修改类的结构,添加新的实现的方法,而该类修改之后,所有继承Bus的子类都需要做相应的修改以适应新的Bus结构,但是让我们来看看Go的方法,只需要在代码中加入下面这些即可,不需要对其它已有的代码做任何修改:

// 新的项目需求
type PersonalSpaceLaw interface {
    IsCampliantWithLaw() bool
}
func (bus Bus) IsCampliantWithLaw() bool {
    return ( bus.l * bus.b * bus.h ) / (bus.rows * bus.seatsPerRow) >= 3
}

完整代码为:

package main
import "fmt"
// Go第一步:定义你的数据结构
type Bus struct {
    l, b, h int
    rows, seatsPerRow int
}
// Go第二步:定义一个现实世界对象的抽象层
type Cuboider interface {
    CubicVolume() int
}
// Go第三步:实现接口的方法
func (bus Bus) CubicVolume() int {
    return bus.l * bus.b * bus.h
}
// 重复上面的二三步
type PublicTransporter interface {
    PassengerCapacity() int
}
func (bus Bus) PassengerCapacity() int {
    return bus.rows * bus.seatsPerRow
}
// 新的项目需求
type PersonalSpaceLaw interface {
    IsCampliantWithLaw() bool
}
func (bus Bus) IsCampliantWithLaw() bool {
    return ( bus.l * bus.b * bus.h ) / (bus.rows * bus.seatsPerRow) >= 3
}
func main() {
    bus := Bus{
        l:10, b:6, h:3,
        rows: 10, seatsPerRow: 5}
    fmt.Println("Cubic volume of bus: ", bus.CubicVolume())
    fmt.Println("Maximum number of passengers:", bus.PassengerCapacity())
    fmt.Println("Is campliant with law:", bus.IsCampliantWithLaw())
}

输入结果为:

Cubic volume of bus: 180 Maximum number of passengers: 50 Is campliant with law: true

接口是一种存在于具体实现与预期实现之间的约定,接口定义了某一种函数或者方法应该怎么执行,需要接收什么样的参数,返回什么样儿的结果,而由各种不同的函数或者方法去做具体的实现,每一个实现同一个接口的函数则不需要做完全一样的处理。

以我们的生活来做一个比法,买东西付钱这是一个接口,但是具体去实现这个接口的人,则有可能会出现各种各样不同的实现方式,比如我刷卡支付,而你却是现金支付,还可能有一个他使用的是银行汇款,但是不管使用的是什么方式,我们都实现了买东西付钱这个接口。

在软件中,接口遵循与我们生活中的这种现像类似的模式,本文我将介绍在Go中如何有别于其它的如 C/C++、Java等面向对象的编程语言不一样的方法来实现接口。Go的接口的实现虽然与其它你所熟悉的语言有很大的不同,但是它工作得却非常的好。

我们的示例的需求是求各种各样形状的面积,比如矩形/方形/圆等等,因为形状可以做为一个接口,它只是一个抽象的概念,而实际形状,则是形状接口的不同实现,我们我们就可以获得一个统一的获取不同形状的面积的方法。如果你还没有任何编程经验,那可能到现在为止对接口的理解可能还是有一些模糊,但是你应该要知道接口是一种十分强大的编程思想——想想,在全世界都有同样的一个需求的时候,我们定义了一个统一的接串口,这样,你不管走到哪里都能以同样的方式完成,虽然有可能完成的过程不一样,但是你总是知道给了钱就可以拿到自己购买的商品,

Go Structs 中的方法Go 中的 Struct——无类实现面向对象编程我们已经有过对长方形面积的求取方式,我们创建了一个名为 Rectangle 的Struct,然后绑定了一个 Area函数到Rectangle 上来求取Rectangle实例的面积。但是代码却没有接口,所以,Area方法只能被 Rectangle使用,现在让我们将其扩展成为接口。

package main
import "fmt"
// Shaper是一个接口,它只有一个函数 Area返回一个int类型的结果
type Shaper interface {
    Area() int
}
type Rectangle struct {
    length, width int
}
// 该 Area 函数工作于 Rectangle 类型,同时与 Shper有同样的定义,现在就称为Rectangle实现了Shaper接口
func (r Rectangle) Area() int {
    return r.length * r.width
}
func main() {
    r := Rectangle{length: 5, width: 3}
    fmt.Println("Rectangle r details are: ", r)
    fmt.Println("Rectangle r's area is: ", r.Area())
    s := Shaper(r)
    fmt.Println("Area of the Shape r is: ", s.Area())
}

输出为:

Rectangle r details are:  {5 3}
Rectangle r's area is:  15
Area of the Shape r is:  15

在如 Java 或者C#这些语言中,一个类型或者类如果需要实现某个接口,需要这样的:

public class Rectangle implements Shaper { // implementation here }

但是在Go中,我们并不需要像上面Java或者C#那样声明一个实现,你可以看到代码中,Shaper接口仅仅只是被定义了,它有一个方法 Area() int*,而 Rectangle 类型也有一个与 Shaper 中 Area 完全一样的方法声明,名称与返回类型以及接收的参数(这里不接收任何参数)都是一样的,所以Go自动的就明白了 Rectangle 实现了 Shaper 接口,所以,你可以将 Rectangle 丢 Shaper ,像这样的: *s := Shaper®,这样使用 *s.Area() 同样也可以得到该形状的面积了。

上面的其实与我们以前写的示例是一样的,现在来增加一个状态 Square :

package main
import "fmt"
// Shaper是一个接口,它只有一个函数 Area返回一个int类型的结果
type Shaper interface {
    Area() int
}
type Rectangle struct {
    length, width int
}
// 该 Area 函数工作于 Rectangle 类型,同时与 Shper有同样的定义,现在就称为Rectangle实现了Shaper接口
func (r Rectangle) Area() int {
    return r.length * r.width
}
type Square struct {
    side int
}
func (sq Square) Area() int {
    return sq.side * sq.side
}
func main() {
    r := Rectangle{length: 5, width: 3}
    fmt.Println("Rectangle r details are: ", r)
    fmt.Println("Rectangle r's area is: ", r.Area())
    s := Shaper(r)
    fmt.Println("Area of the Shape r is: ", s.Area())
    sq := Square{5}
    fmt.Println("Square details is: ", sq)
    fmt.Println("Area of square sq is: ", sq.Area())
    sqs := Shaper(sq)
    fmt.Println("Shaper sqs area is: ", sqs.Area())
}

结果输出为:

Rectangle r details are:  {5 3}
Rectangle r's area is:  15
Area of the Shape r is:  15
Square details is:  {5}
Area of square sq is:  25
Shaper sqs area is:  25

在上面的示例中,Square 与 Rectangle 都实现了 Shaper 接口,虽然 Square 只有一个字段 side,而 Rectangle 有 length 与 width 两个,但是 Shaper 却知道在使用 Square.Area() 时,具体使用哪一种计算方法,下面我们来看一个更简单的输出,因为 Square 与 Rectangle 都实现了 Shaper 接口,所以我们可以将上面程序的 main() 函数修改成为下面这样的:

func main() {
    r := Rectangle{length: 5, width: 3}
    q := Square{side:5}
    shapes := [...]Shaper{r, q}
    fmt.Println("Looping through shapes for area ...")
    for n, _ := range shapes {
        fmt.Printf("Shape detail: %v , and is area is: %dn", shapes[n], shapes[n].Area())
    }
}

它的输出为:

Shape detail: {5 3} , and is area is: 15
Shape detail: {5} , and is area is: 25

继承 让一个类型自动的获取到它的父类的所有行为与属于,而多继承则使得一个类型可以自动的获取到多个父类的所有行为与属于,比如,以前我们有相机,后来有了手机,再到现在,我们所有人用的都是带相机的手机,那么,相机是一个父类,它具有属于相机所特有的属性与行为,比如它有镜头,可以拍照片;手机又是一个父类,它具有相机没有的行为,比如打电话,发短信,但是它又不具有相机的一些行为,比如拍照片等;而我们的相机手机,它同时继承自相机与手机,所以它就同时具有了打电话,发短信而且还能拍照片的功能。

在其它语言中,我们通常都是通过定义接口的方式来实现这种能力,但是在Go中,我们与单继承的作法是一模一样的,继承多少父类,就只需要多少个匿名的父类字段即可,比如上面关于手机、相机以及相机手机的例子,使用Go来实现的话,可以是这样的:

package main
import "fmt"
type Camera struct {}
func (_ Camera) takePicture() string {
    return "Clicked"
}
type Phone struct {}
func (_ Phone) call() string {
    return "Ring Ring"
}
type CameraPhone struct {
    Camera
    Phone
}
func main() {
    cp := new(CameraPhone)
    fmt.Println("A CameraPhone can take a picture: ", cp.takePicture())
    fmt.Println("A CameraPhone also can make a call: ", cp.call())
}

输出结果为:

A CameraPhone can take a picture:  Clicked
A CameraPhone also can make a call:  Ring Ring

我们可以看到,CameraPhone*的实例*cp*同时具有了 *Camera 的 takePicture 方法,也具有了Phone 的 call 方法。

如果你是一个有着其它面向对象语言开发经验的开发者,那么肯定知道什么是继承与子类了,简单来说,他就是一种类型继承另一种类型的方法、行为与属性,继承者具有被继承者全部的方法与属性,比如一个公司的“职员”它继承人大类“人”,职员具有人的所有属性与行为,一辆法拉利具有一辆人所具有的所有属性等等例子数不胜数,但是虽然阿斯顿·马丁也具有一辆车的所有属性与行为,却与法拉利是不一样的。

我们定义一辆车的行为,那么法拉利与阿斯顿·马丁都会具有这种行为,比如“车都是具有发动机的”或者“车都有方向盘或者其它能控制方向的东西”,但是如果我们定义了法拉利的行为和属性则并不一定会导致车或者阿斯顿·马丁也具有同样的行为与属性,比如1947年5月25日,法拉利自己设计并制造的运动车获罗马最高奖,但是这并不代表阿斯顿·马丁也有着同样的这个历史属性。

从上面这些示例中可以看到,子类继承父类的一些行为与属性,子类的行为与属性不一定会同时在父类上也体现,也并不一定会体现在继承自同一父类的其它子类上,我们将上面关于法拉利或者阿斯顿·马丁与汽车的例子体现到程序上面来,那将是这样的一种情况:我们定义一个Struct,其名为Car*,它具有一个方法 *numberOfWheels() 可以获取到车的轮胎数量,我们再定义一个StructFerrari 继承自 Car ,那么我们就可以使用 Ferrari.numberOfWheels() 方法来获取到 Ferrari 的轮胎数量。

我以前的文章Go Struct中的匿名字段 与 Go Structs 中的方法 介绍过,通过匿名字段可以实现上面的要求:

package main
import "fmt"
type Car struct {
    wheelCount int
}
// 定义 Car 的行为
func (car Car) numberOfWheels() int {
    return car.wheelCount
}
type Ferrari struct {
    Car // 匿名字段 Car
}
func main() {
    f := Ferrari{Car{4}}
    fmt.Printf("一辆法拉利有 %d 个轮胎n", f.numberOfWheels())
}

输出为:

一辆法拉利有 4 个轮胎

在上面的示例中,我们仅仅只定义了 Car 的行为或者方法,但是当我们将Car作为一个匿名字段作为Ferrari的属性是,Ferrari就自动的具有了Car的所有方法。那么我们如果再创建一个新的 Struct :AstonMartin 其结果是怎么样的呢?

package main
import "fmt"
type Car struct {
    wheelCount int
}
// 定义 Car 的行为
func (car Car) numberOfWheels() int {
    return car.wheelCount
}
type Ferrari struct {
    Car // 匿名字段 Car
}
// 定义只有 Ferrari 专属的方法
func (f Ferrari) sayHiToSchumacher() {
    fmt.Println("Hi Schumacher!")
}
type AstonMartin struct {
    Car
}
//添加只有 AstonMartin 专属的方法
func (a AstonMartin) sayHiToBond() {
    fmt.Println("Hi Bond, James Bond!")
}
func main() {
    f := Ferrari{Car{4}}
    fmt.Printf("一辆法拉利有 %d 个轮胎n", f.numberOfWheels())
    f.sayHiToSchumacher()
    a := AstonMartin{Car{4}}
    fmt.Printf("一辆阿斯顿马丁有 %d 个轮胎n", a.numberOfWheels())
    a.sayHiToBond()
}

输出为:

一辆法拉利有 4 个轮胎
Hi Schumacher!
一辆阿斯顿马丁有 4 个轮胎
Hi Bond, James Bond!

在上面示例中,AstonMartin 与 Ferrari 同时具有 numberOfWheels() 方法,并且又都具有它们自己的方法,但是像下面这样的就不行:

func main() {
    f := Ferrari{Car{4}}
    fmt.Printf("一辆法拉利有 %d 个轮胎n", f.numberOfWheels())
    f.sayHiToBond()
    a := AstonMartin{Car{4}}
    fmt.Printf("一辆阿斯顿马丁有 %d 个轮胎n", a.numberOfWheels())
    a.sayHiToSchumacher()
}

输出为:

./inherit3.go:27: f.sayHiToBond undefined (type Ferrari has no field or method sayHiToBond)
./inherit3.go:30: a.sayHiToSchumacher undefined (type AstonMartin has no field or method sayHiToSchumacher)

我们知道 Go 中的 Struct 可以像其它语言中的类一样,包含数据,同样的,它也可以通过定义方法来保存属性它自己的行为,将一个方法绑定、关联到一个 Struct 或者任何一个其它的类型中是非常简单的,它就像普通的函数定义一样,唯一的不同是你需要为其指定一个类型(type)。

下面这个示例是一个普通的函数定义,它将返回一个类型为 int 的值:

func myFunc() int {
    // code
}

上面定义的函数将不接收任何参数,返回一个 int 类型的值,如果我们要将其绑定或者说关联到一个Struct上面的话,只需要做很少的修改即可:

type myType struct {}
func (m myType) myFunc() int {
    // code
}

让我们现在来扩展一下前面一篇文章《Go 中的 Struct——无类实现面向对象编程》中的那个 Rectangle 示例, 我们为其添加一个名为 Area 的方法,用来计算长方形的面积:

package main
import "fmt"
type Rectangle struct {
    width, length int
}
func (r Rectangle) Area() int {
    return r.width * r.length
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is:", r)
    fmt.Println("Rectangle area is:", r.Area())
}

输出为:

Rectangle is: {4 3}
Rectangle area is: 12

许多面向对象的程序开发语言都有一个类似 this 或者 self 的概念,用它来代表并访问当前的实例,Go没有这样的概念,如果要将某一个函数或者方法绑定到一个Struct中,直接将一个Struct的实例作为参数传递给该方法或者函数,在上面的示例中,r 就是这个传递给方法 Area 的 Rectangle实例,它会在每一次调用 r.Area() 方法时,指向 r 自身。

同样的,你还可以传递Struct的引用,但是这与直接传递实例没有差别,因为Go会自动为你进行转换:

package main
import "fmt"
type Rectangle struct {
    length, width int
}
func (r Rectangle) AreaByValue() int {
    return r.length * r.width
}
func (r *Rectangle) AreaByReference() int {
    return r.length * r.width
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is: ", r)
    fmt.Println("Rectangle area is: ", r.AreaByValue())
    fmt.Println("Rectangle area is: ", r.AreaByReference())
    fmt.Println("Rectangle area is: ", (&r).AreaByValue())
    fmt.Println("Rectangle area is: ", (&r).AreaByReference())
}

输出为:

Rectangle is:  {4 3}
Rectangle area is:  12
Rectangle area is:  12
Rectangle area is:  12
Rectangle area is:  12

在上面的代码中,我们定义了两人个很相似的方法,一个传递实例,一个传递实例的引用,我们同时又使用了实例分别调用它们,再使用了实例的地址调用它们,可以看到,四种方法的结果都是一样的。

下面我们再“绑定”一个名为 Perimeter 的方法来设置长方形的周长:

package main
import "fmt"
type Rectangle struct {
    width, length int
}
func (r Rectangle) Area() int {
    return r.width * r.length
}
func (r Rectangle) Perimeter() int {
    return 2 * (r.length + r.width)
}
func main() {
    r := Rectangle{4,3}
    fmt.Println("Rectangle is:", r)
    fmt.Println("Rectangle area is:", r.Area())
    fmt.Println("Rectangle perimeter is:", r.Perimeter())
}

输出结果为:

Rectangle is: {4 3}
Rectangle area is: 12
Rectangle perimeter is: 14

现在你可能会想到了,既然为自定义的Struct添加方法绑定,扩展它的行为这么简单,那是否我也可以很容易扩展以有的Struct的方法呢?看下面的示例:

package main
import "fmt"
import "time"
func (t time.Time) first3Chars() string {
    return t.Weekday().String()[0:3]
}
func main() {
    t := time.Time{}
    fmt.Println("First 3 Chars is: ", t.first3Chars())
}

输出为:

./structsmethods4.go:4: cannot define new methods on non-local type time.Time
./structsmethods4.go:9: t.first3Chars undefined (type time.Time has no field or method first3Chars)

你只能将方法或者函数绑定到相同的包内的Struct中,如果你真的需要扩展其它包,那再想想还有什么办法?其实很简单,那就是使用Go Struct中的匿名字段

package main
import (
    "fmt"
    "time"
)
type myTime struct {
    time.Time // 匿名字段
}
func (t myTime) first3Chars() string {
    return t.Weekday().String()[0:3]
}
func main() {
    m := myTime{}
    fmt.Println("Full Weekday:", m.Weekday().String())
    fmt.Println("First 3 chars:", m.first3Chars())
}

输出结果:

Full Weekday: Monday
First 3 chars: Mon

匿名字段中的方法

在《Go Struct中的匿名字段》这篇文章里面我讲到过了对匿名字段的方法,当 time.Time 成为了 myTime 的一个匿名字段之后,我们就能通过 myTime 访问 Time 所的方法,我们同样可以使用 myTime.String() 将自己转换成为一个字符串,让我们再来做看一看前面的示例。

在下面这个示例中,我们返回到Kitchen与House的问题上面来,我们知道了如何访问匿名字段的数据,那么也应该知道如何访问匿名字段的方法了,他们是一样的:

package main
import "fmt"
type Kitchen struct {
    numOfForks int
    numOfKnives int
}
func (k Kitchen) totalForksAndKnives() int {
    return k.numOfForks + k.numOfKnives
}
type House struct {
    Kitchen
}
func main() {
    h := House{Kitchen{4,4}}
    fmt.Println("Sum of forks and knives in house:", h.totalForksAndKnives())
}

输出结果为:

Sum of forks and knives in house: 8

Go 允许你定义含有无变量名的字段的Struct,这些无名称的字段称之为匿名字段,让我们通过一些示例来了解它们到底是什么,以及它们为什么很有用。

在下面的示例中,我们定义一个名为 Kitchen 的Struct,它仅仅只包含一个名为 numOfPlates 的 int类型的字段用来保存盘子的个数,我还定义了另一个Struct名为 House ,它包含了一个 Kitchen 的实例,但是我没有为其指定字段名,所以,它是一个匿名字段。

package main
import "fmt"
type Kitchen struct {
    numOfPlates int
}
type House struct {
    Kitchen // 匿名字段
    numOfRooms int
}
func main() {
    h := House{Kitchen{10}, 3} // 初始化需要使用Struct名
    fmt.Println("房屋 h 有", h.numOfRooms, "间房") // numOfRooms 是 House 的一个字段
    fmt.Println("房屋 h 有", h.numOfPlates, "个盘子") // numOfPlates 是Kitchen提供的字段,而Kitchen又是House的一个匿名字段,所以这里可以访问到它
    fmt.Println("这间房屋的厨房有:", h.Kitchen) // 我们可以通过Struct的名称访问整个匿名Struct的内容
}

输出结果为:

房屋 h 有 3 间房
房屋 h 有 10 个盘子
这间房屋的厨房有: {10}

首先我们需要注意到的事情是,当我们定义了 Kitchen 为House的一个匿名字段之后,我们将可以直接通过Kitchen中的字段名称访问Kitchen中字段的值,这同样也“成了”House的字段,但是如果像在Java这样的语言中,我们做一下对比,它需要像下面这样的去实现同样的功能:

public class Kitchen {
    public int numOfPlates;
}

public class House {
    public Kitchen kitchen;
}

public static void main(String[] args) {
    House h = new House();
    h.kitchen.numOfPlates = 10; // 引用自字段的子字段
}

其次我们需要知道,匿名字段的完整的实例数据是可以被访问到的,需要通过它的Struct名称,在本示例中,通过 h.Kitchen 来访问获得,所以,如果你想打印出 Kitchen 中的 Plate 数量的话中,可以这样:

fmt.Println(h.Kitchen.numofPlates)

最后,要注意到,初始化的时候,匿名字段的Struct名称是不允许省略的,它必须像这样:h := House{Kitchen{10}, 3}*,而下面这样的写法都是错误的:*h := House{{10}, 3} 或者 h := House{10,3}。

匿名字段发生命名冲突

很有可能两个不同的Struct具有同样名称的字段,而一个Struct又是另一个Struct的匿名字段,这就产生的命名冲突的问题,当这个问题出现的时候,外层的Struct中的字段可以像以前一样直接被访问到,但是其匿名字段就需要通过Struct名称访问了。

在下面的示例中,我们的 House 与 Kitchen 同时具有 numOfLamps 字段,但是House是外层的struct,它的 numOfLamps* 字段将隐藏掉Kitchen的 *numOfLamps*,如果你还是需要访问Kitchen的numOfLamps值,那么需要通过引用它的Struct名:*h.Kitchen.numOfLamps :

package main
import "fmt"
type Kitchen struct {
    numOfLamps int
}
type House struct {
    Kitchen
    numOfLamps int
}
func main() {
    h := House{Kitchen{2}, 10} // Kitchen 有2个Lamps,House有10个
    fmt.Println("House有", h.numOfLamps, "个Lamps")
    fmt.Println("Kitchen有", h.Kitchen.numOfLamps, "个Lamps")
}

输出为:

House有 10 个Lamps
Kitchen有 2 个Lamps

所以从上面可以看到,对于不同层级的相同名称的字段,Go制定了相应的规则,但是对于同一层级的相同名称的字段,则需要我们自己去解决,在下面的示例中,Kitchen与Bedroom都具有 numOfLamps 字段,并且它们又都是House的匿名字段,现在,如果我们引用 House.numOfLamps,Go编译器就会不知道你到底是要使用哪一个 numOfLamps值:

package main
import "fmt"
type Kitchen struct {
    numOfLamps int;
}
type Bedroom struct {
    numOfLamps int;
}
type House struct {
    Kitchen
    Bedroom
}
func main() {
    h := House{Kitchen{2}, Bedroom{3}} // Kitchen 2, Bedroom 3
    fmt.Println("Ambiguous number of lamps:", h.numOfLamps) // 这会出错
}

输出为:

./anonymousfields3.go:15: ambiguous selector h.numOfLamps

因为Go不知道你到底是要 Bedroom 的还是 Kitchen 的 numOfLamps,所以,它直接就提供值了,结果就是返回 h.numOfLamps 有歧义。

解决这个问题的办法是通过匿名字段的Struct名称访问其值:

fmt.Println("House中Lamps的数量为:", h.Kitchen.numOfLamps + h.Bedroom.numOfLamps)

输出为:

House中Lamps的数量为:5

星期五, 12/28/2012 - 16:22 — matrixstack

Go是非面向对象的,而是面向过程的,但是在当今程序开发的世界里,使用最为广泛的还是面向对象编程,这或许会让你感觉Go作为一个新生的语言最应该有的一面却没有被支持,但是其实它只是用了自己的方法,在本文里,我将介绍在Go里面,如何实现面向对象(Go提供了面向对象所提供的一切,或许提供得更多)。

如果你一直都是一个“类”用户,那么转到Go的“面向对象”上来,可能还需要一些观念上的转变,这或许很难,但是一旦你转变过来了,你就会发现,Go的方式有多么的强大了。

在了解Go的面向对象之前,一定要在心里面知道:Go是没有类的,这是你进入Go的面向对象世界的第一步,也是最重要的一步;另一个你知道知道的是:Go不是面向对象的,只是他的实现类似于面向对象,我们来看一个简单的示例,它能说明一切:

Java代码:

class House {
    public String getHouseName() { // 方法定义在类里面
        // 实现
    }
}

Go 代码:

type House struct { }
func (h House) GetHouseName() string {} //方法被定义在 Struct 外,但是却作用于 House

现在让我们来学习一下如何创建 Struct 以及如何通过它实现“面向对象”,一个 Struct 通过关键字 type 与 struct 定义:

type my_struct_name struct { }
type Rectangle struct { }
type Vehicle struct { }
type VehicleCar struct { }

上面的所有定义都是合法的,但是下面这样是不合法的:

type Hash# struct { } // 名称不允许包含特殊字符
type 0Struct struct { } // 名称不允许以数字开始

当然了,Struct 还能存储其它数据,所以,在这方面它类似于 Class,可以用来定义真实世界中存在的实体:

type mystruct struct {
    i int
    j int
    s string
}

type Rectangle struct {
    length, width int //你可以在一行中通过逗号分割的方式创建多个同类型的项目
    area float64
}

下面这个示例展示了一个 Struct 的实际应用:

package main
import "fmt"
// 定义 Rectangle 结构
type Rectangle struct {
    length, width int
}
func main() {
    r := Rectangle{}
    fmt.Println("默认的长方形是:", r) // 打印出默认的 Rectangle 的归零值
}

输出为:

默认的长方形是: {0 0}

这里有一个重要的需要注意的事情是:struct中的变量的值都是归零的,也就是说,int 类型的值将为 0,string 类型的值将为空字符串等等,所以,每一个你自己定义的Struct 也都是有一个属于其自己的归堆值的。下面这个示例中,我将展示另一种方式初始化 Struct——在创建它的时候就指定初始值:

package main
import "fmt"
type Rectangle struct {
    length, width int
    name string
}
func main() {
    r1 := Rectangle{2, 1, "First Rectangle"}
    fmt.Println("第一个长方形 r1 为:", r1)
    r2 := Rectangle{width: 3, name: "Second Rectangle", length: 4}
    fmt.Println("第二个长方形 r2 为:", r2)
    pr := new (Rectangle) // 获取一个指向一个新的长方形的指针
    (*pr).width = 6 // 设置 pr 的宽为 6
    pr.length = 8 // 与上面是同样的效果,看这里是没有 -> 符号的
    pr.name = "指针长方形"
    fmt.Println("指针长方形是:", pr)
}

输出为:

第一个长方形 r1 为: {2 1 First Rectangle}
第二个长方形 r2 为: {4 3 Second Rectangle}
指针长方形是: &{8 6 指针长方形}

这里需要注意的事情是:

  • 你可以只将值按 struct 定义时的顺序赋给它的成员,但是需要放在大括号里面,并且每一个值需要使用逗号分开;
  • 如果你给定值的时候还提供了名称,那么就不受顺序的限制;
  • 你可以通过 new 获取到一个新创建的 struct 的指针;
  • 这样的一个指针可以不需要 *操作符;
  • Go默认提供根据Struct的值转换成为字符打印的功能

封装以及 Struct 及其变量的访问

其它的编程语言使用一些特别的关键字(比如 public、private、package‘protected等)定义类中不同的属性与变量的可访问性,当我在使用Go之前,我认为所有的这些都是言存在的,但是之后,我发现,原来实现变量与属性的访问控制可以如此简单:

type notExported struct { // 以小写字母开头命名的struct只能在同一个包内调用,我们称这为不导出
}
type Exported { // 大写开头则可导出,也就是任何包都可以访问
    notExportedVariable int // 小写字母开头不允许包外访问
    ExportedVariable int // 大写字母开头则允许包外访问
    s string // 不导出
    S string // 导出
}

所以,你应该明白,在Go里面命名都应该是统一规定的,它要求你使用驼峰式的命名,是否使用大小写开头则要看是否需要导出。

for 声明是 Go 中唯一的一个循环声明,普通的用法如下:

for "initialization statements"; "bool expression that has to evaluate to true"; "statements run prior to every loop except the first" {
    // Code to be executed if the boolean expression evaluates to true
}

翻译过来为:

for "初始化声明"; "条件判断,为真是循环才执行"; "除第一外每一次循环都会执行的声明" {
    // 循环主体
}

上面的三个部分中,任何一个部分都是可以不写的,但是即使不写,那个属于它的结束符的分号却还是必须要写,除非三个声明都没有的时候,所有的分号都可以一并省略,请看下面的示例:

package main
import "fmt"
func main() {
    // 初始化i为0;每一次循环之后检查i是否小于5;让i加1
    for i := 0; i < 5; i++ {
        fmt.Println("i现在的值为:", i)
    }
}

执行输出结果为:

cox@Cox:~/workspace/go/src/gotutorial$ go run forloop1.go
i现在的值为: 0
i现在的值为: 1
i现在的值为: 2
i现在的值为: 3
i现在的值为: 4

从上面的结果可以看到,我们在定义 i 的初始值为0之后,在循环主体第一次执行前,i++是没有运行的,所以我们来能取得它的初始值 0。下面这些循环体你都可以在自己的示例中去运行:

// 没有判断声明,循环将永远不停的执行
for i := 0; ; i++ {
    fmt.Println("i现在的值为:", i)
}
// 因为 i 的值不会变化,而i的初始值小于3,所以,它将永远小于3,那么循环加永远运行
for i := 0; i < 3; {
    fmt.Println("i现在的值为", i)
}
// 虽然我们没有在 *for* 中声明初始值以及变量的变量方式,但是我们把初始化移到for循环外面,然后把初始变量的变化声明移动到循环里面也是可以的,下面代码的输出将为:
// s的值为:
// s的值为: a
// s的值为: aa
// s的值为: aaa
// s的值为: aaaa
s := ""
for ; s != "aaaaa"; {
    fmt.Println("s的值为:", s)
    s = s + "a"
}

在 Go 的 for 循环里面,你可以同时初始化多个变量,多个条件判断或者同时更新多个值,请看下面的示例:

package main
import "fmt"
func main() {
    for i, j, s := 0,5,"a"; i < 3 && j < 100 && s != "aaaaa"; i, j, s = i+1, j+1, s + "a" {
        fmt.Printf("i=%d, j=%d, s=%sn", i, j, s)
    }
}

输出:

i=0, j=5, s=a
i=1, j=6, s=aa
i=2, j=7, s=aaa

第三次循环之后,虽然 i < 100 与 s != "aaaaa" 都成立,但是 i < 3 已经不成立,所以循终止。

break 关键字

break 的作用是让循环在它所出现的地方终止,然后执行该循环后面的下一行代码或指令,如下示例:

package main
import "fmt"
func main() {
    i := 0
    for {
        if i >= 3 { break }
        fmt.Println("当前i的值为:", i)
        i++
    }
    fmt.Println("for后的第一个声明")
}

输出为:

当前i的值为: 0
当前i的值为: 1
当前i的值为: 2
for后的第一个声明

continue 关键字

continue 中止当前循环,然后回到循环的起始进行下一步循环,如下示例:

package main
import "fmt"
func main() {
    for i := 0; i < 11; i++ {
        if i%2 == 0 {
            continue //我不输入偶数
        }
        fmt.Println("当前i的值为:", i)
    }
}

输出为:

当前i的值为: 1
当前i的值为: 3
当前i的值为: 5
当前i的值为: 7
当前i的值为: 9

range 关键字

range 关键字让你可以遍历数据/Map,为了便于理解,你可以把它理解为“每一个XX的索引”,当你使用它对数组或者片段遍历时,每一次返回它们的索引,对于MAP遍历时,每一次返回它的键,如下示例:

package main
import "fmt"
func main() {
    // 对于数组,range 返回数据的索引
    a := [...]string{"a", "b", "c", "d"}
    for i := range a {
        fmt.Println("数组的第", i+1, "个值为:", a[i])
    }

    // 对于 map,返回每一个键
    capitals := map[string]string {"France":"Paris", "Italy":"Rome", "China":"Beijing" }
    for key := range capitals {
        fmt.Println("Capital of", key, "is", capitals[key])
    }
}

输出为:

数组的第 1 个值为: a
数组的第 2 个值为: b
数组的第 3 个值为: c
数组的第 4 个值为: d
Capital of Italy is Rome
Capital of France is Paris
Capital of China is Beijing

同时,range还可以同时返回两人个值,第一个值为索引或者键,第二个值为数组或者Map的值本身,比如上面的两人个循环我们可以改写成为下面这样也可以得到同样一输出:

package main
import "fmt"
func main() {
    // 对于数组,range 返回数据的索引
    a := [...]string{"a", "b", "c", "d"}
    for i,v := range a {
        fmt.Println("数组的第", i+1, "个值为:", v)
    }

    // 对于 map,返回每一个键
    capitals := map[string]string {"France":"Paris", "Italy":"Rome", "China":"Beijing" }
    for key,value := range capitals {
        fmt.Println("Capital of", key, "is", value)
    }
}

程序需要根据不同的条件进行不同的处理, 我们程序不可能只按着某一个顺序一路执行下去,比如我们经常会遇到的这样一种情况:如果某一个文件存在,则将新内容更新至已有文件,如果文件不存在,则先创建文件,这个时候我们就可以使用if - else 控制结构了,在Go中,该控制结构如下:

if some_boolean_expression {
    // 如果 some_boolean_expression 为 true,则会执行这里
} else if alternate_boolean_expression {
    // 如果 some_boolean_expression 为 false 并且 alternate_boolean_expression为 true,则执行这里
} else {
    // 如果上面的所有判断都为 false ,才能执行这里
}

注意事项 :

  • 在上面的结构说明,some_boolean_expression 以及 alternate_boolean_expression 的计算结果只能是 true 或者 false 里面的一个,不能为其它的值;
  • 对于判断声明,括号不是必须的,你可以为其添加一个括号,但是这是没必要的;
  • 与其它语言不同的是,Go要求左花括号必须与 if 或者 else 处于同一行内,就会报错,详情看写 GO 程序中一些常见的语法和其它错误
  • int 类型的数据不能作为判断声明的结果被 if-else使用。

下面这些是条件判断的比较操作符:

  • == : 等于
  • != : 不等于
  • < : 小于
  • <= : 小于或等于
  • > : 大于
  • >= : 大于或等于
  • && : 和(并且/与)
  • || : 或

下面是一个最很简单的示例:

package main
import "fmt"
func main() {
    if true {
        fmt.Println("这里会执行")
    }
    if false {
        fmt.Println("这里不会执行")
    }
}

输出很很简单,就是打印出“这里会执行”这五个字符,下面这个示例则使用了一些判断操作符:

package main
import "fmt"
func main() {
    a, b := 4, 5
    if a < b {
        fmt.Println("a < b")
    } else if a > b {
        fmt.Println("a > b")
    } else {
        fmt.Println("a = b")
    }
}

输入结果为 a < b,对于有其它语言编程经验的人来说,下面这些错误可能会经常出现:

// 非布尔值 5
if 5 {}
// 非布尔值 s
var s string
if s {}
// 非布尔值 e
var e os.Error
if e {}
// if 声明与左大括号不在一行内
if true
{}
// else 与 右大括号不在一行
if true {
}
else {
}

本文是 「Golang.org」的原文档翻译,出于本人有限的英语水平,如有不当,还请指正。

Go 是一个全新的语言,当然,它也从很多以存在多年的优秀语言中吸取了很多优秀的特性,但是因为其自身的特殊性,使得 Go 程序与其它相关的语言所开发的出来的程序有很大的不同,直接将C++或者Java程序翻译为Go程序并不会让你得到一个满意的结果——Java程序永远都只是Java程序,它成不了Go程序。另一方面,从Go的角度去想一个问题,你会得到一个可以成功解决它但是和其它语言完全不一样的方式。换句话说,想写出漂亮的Go程序,你必需要了解它的特性和风格,甚至是已有的约定,比如命名规范、代码格式化结构等,了解这些,可以让你的程序更简单地其它人看懂。

本文档给出了一些能让你写出更加干净的符合Go语言习惯的代码,但是建议你在阅读本文之前先阅读一下Language Specification、Tour of Go以及「如何写Go程序(How to Write Go Code)」.

示例

Go Package Sources 并不只包含了它的核心库,同时它还包含了很多使用该语言写的示例程序,如果你有关于如何使用Go解决问题或者在Go应该如何实现的相关问题,这些示例代码或话会给你答案,它还能告诉你,你应该将自己的想法以怎样的一种方式转换成Go程序。

格式化

格式化应该是最具有争议但是也是最重要的一个Go语言的特性,每一个开发者可以有属性自己的书写格式,但是所有人都使用同一种格式看起来肯定是最好的,这使得我们不再需要花费太多时间放在了解别人的书写风格上。

在Go中,不同于其它语言,它让计算机程序来管理着代码的格式化,gofmt 命令(也可以使用 go fmt)可以读取Go代码文件,并将它们以标准格式对其进行格式化,包括它们的缩进、空格等,请看下面这个示例:

type T struct {
    name string // name of the object
    value int // its value
}

gofmt 会将其格式化为:

type T struct {
    name    string  // name of the object
    value   int     // its value
}

所有Go标准库中的代码都已经通过gofmt进行了格式化。

比如下面这些Go中最常见的会使用到的格式化约定:

  • 缩进:Go使用制表符(Tabs)进行缩进,而不是很多语言中都推荐的空格,gofmt默认会使用制表符进行缩进,你应该仅仅在需要空格的地使用空格。
  • 行长度:Go没有行代度限制,所以,你完全不用担心这个问题,但是如果你感觉某一个行实在是太长了,那你可以将其截断,然后使用一些制表符让它看起来和它原本应该属于的那一行开始于同一个地方。
  • 括号:Go只有很少的一些地方需要括号,结构控制(比如if, for, switch等的条件判断)都是不需要括号的,操作符的优先级层次更短更清晰,所以,x << 1 + y < 6,就可以很明白的知道这是什么了。

注释

Go同时对G风格的块注释 /* ......*/ 和 C++风格的行注释 // .....提供支持,行注释是标准的,而块注释一般只建议使用的对包的注释上或者使用在需要禁用(但不是删除)一大段代码的功能上。

godoc程序(同时也是文档服务器)会从Go代码中提供包的说明文档,Comments that appear before top-level declarations, with no intervening newlines, are extracted alog with the declaration to serve as explanatory text for the item. The nature and style of these comments determines the quality of the documentation godoc produces.

任何一个包都应该包含一个包注释——一个用来说明包功能以及使用方法等信息的块注释,对于被分割为多个文件的包,包注释只需要出现在某中的任何一个文件中即可,godoc会将它作为所有同一个包的说明文档。The package comment should introduce the package and provide information relevant to the package as a whole. It will appear first on the godocpage and should set up the detailed documentation that follows.

/*
    Package regexp implements a simple library for
    regular expressions.

    The syntax of the regular expressions accepted is:

    regecxp:
        concatenation { '|' concatenation }
    concatenation:
        { closure }
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

如果某个包是一个十分简单的包,那么这个包的注释可以很简单:

// Package path implements utility routines for
// manipulating slash-separated filename paths.

注释不需要进行特殊的格式化,文档创建工作不能确定使用的是否是固定宽度的字体,所以,你不需要使用空格去对注释进行格式化——godoc像gofmt一样,它会帮助你完成这些格式化。注释应该仅仅保是纯文本的字符串,所以,HTML或者其它的标记方式(比如 THIS)都是不应该被使用的,godoc只会照字面的意思去解决,所以,请尽可能让你的注释内容就是你想表达的内容的文本——使用正确的拼写,合理的分段,长句进行合理的截段等。

在包内,任何一个仅跟着一个声明的注释都被当作该声明的注释文档,任何一个可导出的(大写开头的命名)的声明都应该具有一个说明注释,而这个注释文档最好也有遵循一定的约定,它的最第一句话应该是一个以该声明名称开始的用来概括该声明作用的句子。

// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {...}

Go 的声明语法允许分组声明,这使得一个单一的文档注释可以说明一组相关的常量或者变量,整个声明可以像下面这样来做(这是懒人做法啊):

// Error codes returned by failures to parse an expression.
var (
    ErrInternal     = errors.New("regexp: internal error")
    ErrUnmatchedLpar= errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar= errors.New("regexp: unmatched ')'")
    ...
)

即使是私有名称,分组也是可以指明它们之间的关系的,比如下面这一组私有变量由一个互斥锁保护:

var (
    countLock   sync.Mutex
    inputCount  unit32
    outputCount unit32
    errorCount  unit32
)

名称

与其它语言一样,名称在Go中也是十分重要的,在某些环境下,一个名称甚至还需要其特有的语义——对于一个实例,它的方法或变量是否能被外部调用和访问取决于那个方法或变量的名称是否以大写字母开始,PublicItem这样的就是可以被外部访问的,而 privateItem则只能放在家里自己使用,也就是说:大写开始表示可以被外部访问,也就是可以被导出,而小写开头则只能是私有的。

包名称

当一个包被导入之后,包名称就成为一个访问该包资源的入口,比如,当我们:

import "bytes"

之后,我们就可以请求 bytes.Buffer了,一个简洁、明了令人回味的包名称可以帮助他人在使用该包的过程中更加方便快乐。我们约定包名称应该是一个“仅仅小写字母的单个字汇字符串”,我们不必要使用下划线或者驼峰式的写出一个完整的长的名称,你要想想,假如你自己在使用别人的某一个包时,需要像fooBarFooBar.FooBar这样请求它的资源,你会是一种什么样的感觉,我们只需要简单的foo.Bar即可。你不必要担心你所选择包名可能与其它已有的包名相同,因为Go允许在导入一个包时为其指定一个本地使用的别名。

另一个约定是“包名是该访问该包所有资源的基础入口”,比如 src/pkg/encoding/base64 包应该以encoding/base64导入,它的名称为 base64,而不是 encoding_base64或者encodingBase64。

包导入工具将使用名称来引用它的内容(import . 只在某些特殊用途的时候使用,如非必要,请尽可能的不去使用),所以,包中可导出的内容就可以以最直接方式访问到,对于实例,如bufio包中的Reader,就可以直接使用 bufio.Reader访问到,而不是 BufReader或者其它的名称,因为用户看前者会更直观。由于导入的实体总是以其完整的位置访问,所以 bufio.Reader不会与 io.Reader 混淆。相似的,用来创建新的 ring.Ring实例的构建函数,通常我们可能会将其命名为 NewRing,但是,因为 Ring 是 ring 导出的唯一一个类型,所以,我们也只能创建ring.Ring 这一种类型的数据,所以,直接将该构建函数取名为 New即可。

另一个*简化名称*的示例是 once.Do,我们不将其命名为 once.DoOrWaitUntilDown,长的名称在绝大多数时候并不会让我们更容易理解其所指代的意思,要让使用者更方便的调用也让他们更好的了解其使用方法或者意思,那好的办法是选择简洁的名称,并为其写上足够说明其意思和使用方法的注释。

Getters

Go 不会自动的提供 getters 和 setters 函数,但是这种事情你完全可以自己去完成,但是你也应该在适当的时候这么做,但是,你不需要把 Get 写进你所定义的方法名称中去,比如你现在有一个字段 owner(注意这是小写字母开头的,所以不导出,外部也部无法直接调用),它的 getter 名称应该称之为 Owner而不是GetOwner,它成为了获取 owner 字段数据的钩子,而 setter 函数则应该称之为 SetOwner:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

接口名称

Go 约定接口名称为方法名称后面加上 er:Reader、Writer、Formatter等,Go中有许多这样名称的标准接口,也有很多方法去实现了这些接口,它们这些方法的名称都是像 Read、Write、Close、Flush、String等这样的,为了避免造成混乱,在你自己的程序中尽可能的不要去使用上面这些标准库中提供了的方法或者接口名称。

相反的,如果你的程序实现了一个与某一个已经被另一个大家所熟知的程序实现过的接口,那么你最好是使用该程序所使用的方法名称,一个将你的数据转换为字符的方法,不要叫作ToString,而应该直接叫作 String。

多词汇名称

最后,Go约定使用“驼峰式”的命名,而不使用下划线的方式,比如MixedCaps或者mixedCaps而不是mixed_caps。

分号

与 C 一样,Go使用分号“;”来终止一条语句,而与C不一样的是这些分号一般是不会出现在的源代码中的,Go使用一种简单的规则在其扫描代码的过程中合适的插入这些分号,所以,你在写代码的时候完全不需要将其明显的写入进源代码中。

Go所约定的规则是这样的:如果一个新行的最后面是一个类型识别码(一般都是 int 或者 float64这样的),一个常量(比如数字或者字符串)或者下面这些关键字或者符号中的任何一个:

break continue fallthrough return ++ -- ) }

Go 会自动在其后面加了分号,上面所说的这些可以简单的描述为:“如果一个新行前面的Token可以终止一个声明,则在该Token后插入分号”。

结束大括号前的分号也是可以省略的,比如:

go func() { for { dst <- <-src } }()

就不需要分号,Go程序只需要在诸如循环、分隔初始值、条件或者延续元素的情况,才需要明显地使用分号,当然,你可以将多行地声明写进同一行中,这个时候每一个声明后就需要使用分号来区分。

有一点你需要特别注意的,你不应该把控制结构(if、for、switch或者select)的左大括号写在下一行,如果你这么做了,那么Go会在这些声明后面加上一个分号,这肯定不是你想看到的。

if i < f() {
    g()
}

而下面这样就是错误的:

if i < f()  // wrong!
{
    go ()   // wrong!
}

控制结构

Go 的控制结构与 C 十分类似,但是有一个重要的不同在于Go没有 do 或者 while 循环,仅仅使用 for 来做这些基本工作,switch 有更多的灵活性以及if和switch可以接受一个初始值声明,它还有一个新的控制结构 select,语法也略有不同:没有括号,主体必须分隔。

if

在Go中一个简单的if语句看起来像下面这样的:

if x > 0 {
    return y
}

强制性的要求 if 声明必须被分隔为多行,由于是在它的主体中包含有如 return或者break等控制语句的情况更为适用。

由于 if 和 switch 支持初始化声明,所以,我们可以在其条件判断前为该条 if 语句初始化一个本地变量:

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

在Go标准库中,如果一个 if 语句不接会导致程序继续执行其下一条语句时,我们可以不需要使用 else ,这通常的都是在if主体中存在 return、break、continue或者goto等等这些能控制代码结构的关键字。

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.close()
    return err
}
codeUsing(f)

在上面的代码中,我们首先打开一个文件,如果没有错误存在,程序会继续往下执行,如果再没有错误,则会执行 codeUsing(f)函数,但是如果有错误出现,那么在if执行之后就会跳出整个程序。

声明重载(Redeclaration)

在上面的示例中,你应该看到,我们的一个操作符 := 将 os.Open 返回的值同时声明给了两人个变量:f和err:

f, err := os.Open(name)

接着:

d, err := f.Stat()

看到,err 在前面声明之后,我们接着又重新声明了它,这表明,在第一个声明中,创建了err并为其赋了值,而在后面一次声明中,仅仅只是为其赋了新值。

在一个为变量 v 赋值的 := 的声明中,那么:

  • 如果在该声明同样的作用域内该声明之前已经存在了对该变量的声明,那么本声明直接为其赋值
  • 没有出现上面的情况,本声明将先创建该变量之后再为其赋值

for

Go的for循环有点像C的for循环——但是是不一样的,它有三种不同的格式:

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

更短的声明让我们更容易给循环一个索引变量:

sum := 0
for i := o; i < 10; i+ {
    sum += 1
}

如果你在遍历一个数组、片(Slice)、字符串或者Map,或者从一个 channel 中读取数据,range 可以被用来控制该循环:

for key, value := range oldMap {
    newMap[key] = value
}

如果你仅仅只需要循环过程中 range 产生的第一个值(range 产生两人个值 key, value),那么直接丢掉第二值即可:

for key := range m {
    if expired(k) {
        delete(m, key)
    }
}

但是如果你仅仅只想要其产生的第二个值,那么需要使用一个“空定义(blank identifier)”作为占位符,它是一个下划线:“_”:

sum := 0
for _, value := range array {
    sum += value
}

这是因为,如果你没有那个占位符,那么 range 还是会首先把它产生的第一个值赋值给 value,而有了那个点位符之后,range将key赋值给,之后会直接把该值丢掉,然后range就赋值其第二值 value给value了。

对于字符串,range能做更多的事情,breaking out individual Unicode characters by parsing the UTF-8. Erroneous encodings consume one byte and produce the replacement rune U+FFFD. The loop

func main() {
        for pos, char := range "编程开发" {
                fmt.Printf(" aracter %c starts at byte position %dn", char, pos)
        }
}

将会打印出:

aracter 编 starts at byte position 0
aracter 程 starts at byte position 3
aracter 开 starts at byte position 6
aracter 发 starts at byte position 9

最后,Go没有逗号操作符,而 ++ 与 -- 是声明而不是表达式,如果你想在一个for循环中使用多个变量,你需要使用 Parallel Assignment。

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

switch

Go 中的 switch 比C中的更加通用,它的表达式并不局限在常量或者数字中,它的 case 是从上到下进行匹配,直到找到一个为 true 的 case,如果你的 if-else-if-else-... 有点儿太多了,那么就使用 switch吧。

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
`   return 0

Go 中没有自动向下执行(Fall Through)机制,所以,你不能写成这个样子:

case i < 0:
case i > 10:
    doSomethingWithI(i)

但是Go可以将多个条件使用逗号分开,所以上面这样在其它语言里面常见的写法在Go你应该写成:

case i < 0, i > 10:
    doSomethingWithI(i)

Switch还可以被用来发现某一个接口变量的动态类型,像下面这样的去实现:

switch t := interfaceValue.(type) {
default:
    fmt.Printf("unexpected type %T", t)
case bool:
    fmt.Printf("boolean %tn", t)
case int:
    fmt.Printf("integer %dn", t)
case *bool:
    fmt.Printf("pointer to boolean %tn", *t)
case *int:
    fmt.Printf("pointer to integer %dn", *t)
}

函数(Functions)

多值返回 (Multiple return values)

Go函数和其它语言最不一样的特性就是它可以在一个函数中同时返回多个值,这个特性能改进很多在C里面要通过各种各样的特殊办法实现的功能,而这个特性你在本文档前面也已经见到过了:

f, err := os.Open(name)

同样的,在os包中还定义的很多比如 Write方法:

func (file *File) Write(b []byte) (n int, err error)(

就像它的文档所说的,该方法返回写入的总字节数,并且当 n != len(b) 的时候返回一个 non-nil 的 error,这种写法在Go很通用,你可以在专门的章节中看到更多的关于 error 处理的示例。

类似的这种方法省却了一个指针的使用,而且它看起来更加符合我们的普通常人的理解(需要返回什么东西就直接返回就是),下面这个示例演示了一个能同时返回两个值的函数,它先从一个字节数组(byte array)中找到一个数字,然后返回下一个为数字的元素的位置:

func nextInt(b [byte], i int) (int, int) {
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

然后你可以像下面这样的去调用它:

for i := 0; i < len(a); {
    x, i = nextInt(a, i)
    fmt.Println(x)
}

带名称的结果参数

Go 函数中的返回结果参数可以被赋予一个名称,它可以像其它的变量一样的在函数中使用,命名之后,当函数执行时,它会被赋予一个其所属的数据类型的0值,而当函数执行一个 return声明之后(不需要带任何参数的声明),该命名了的返回参数当前的值就会被返回。

Go 不强制你必须为返回参数命名,但是这样做可以使你的代码更短——它们被文档化了。如果我们将上面的函数进行命名,那么:

func nextInt(b []byte, pos int) ( value, nextPos int) { ... }

因为命名了的返回结果会在函数执行时被初始化了,并且和一个未经修饰的 return 绑定,所以它能使唤代码更清晰,下面这是一个 io.ReadFull的一个版本:

func ReadFull(r Reader, buf []byte) ( n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

延迟(Defer)

Go的defer声明一个定时运行的函数呼叫(deferred function),这样的函数会在函数终止前立马执行,这种方式在其它方法里面不太常见,但是它却非常有用,比如我们有一个函数要打开一个文件,但是不管这个函数是否执行成功,我们都应该在不再需要这个文件的时候关闭这个文件,这个时候我们就可以在成功打开文件之后,立马定时关闭该文件,当函数再往下执行时,不管是出现什么错误或者是成功执行,文件都会被正确关闭。

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.close() // f.close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err // f will be closed if we return here
        }
    }
    return string(result), nil // f will be closed if we return here
}

延迟请求一个函数的执行至少有两人个好处:首先,它能让你永远不会因为某些错误(这实在是太容易发生了)而关闭文件;其次,关闭与打开文件的代码可以被写在一起,这样后面的代码完全可以放开安排自己的代码结构了(肯定啊,不需要在每一个 return 前面关闭一次文件了,我在任何一个地方可以 return ,文件会自动的关闭)。

传递给被延迟请求的函数的参数是在 defer 执行是就计算出来的,而不是函数执行时才计算的,所以像下面这段代码运行的结果将是 10 0,而不是10 10:

func forDefer() {
    i := 0
    defer fmt.Printf("%d ", i)
    i = 10
    fmt.Printf("%d ", i)
}

同时它还遵循LIFO(Last Input First Output:先进后出)所以,当defer执行之后,延迟函数所调用的参数不管再怎么改变,都不会影响延迟函数本身的输出,所以像下面这段你代码,它最终执行后的结果将不是 0 1 2 3 4,而是4 3 2 1 0:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

延迟函数执行还时常被用在跟踪函数本身的执行上,比如下面的示例:

func trace(s string)    { fmt.Println("entering: ", s) }
func untrace(s string)  { fmt.Println("leaving: ", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something you like as normal...
}

对于这个跟踪函数,我们还可以做得更简洁高效,还记得我们在前面了解到的关于延迟函数的参数的值是在defer执行时就被计算的吗?我们在这里来使用这一特性重新完成前面的那个函数运行跟踪函数:

func trace(s string) string{
    fmt.Println("entering: ", s)
    return s
}

func un(s string) {
    fmt.Println("leaving: ", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    def un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

上面代码的输出结果是:

entering: b
in b
entering: a
in a
leaving: a
leaving: b

对于习惯于以块为单位管理代码的其它语言的开发者来说,defer可以看起来很奇怪,但是它功能的功能及实用性是可以肯定的,而且是十分有趣的,它使得代码不在以块为单位而是以功能来进行组织管理的。

数据

通过 new 分配内存

Go内置了两人函数 make 和 new,它们做的事情是不一样的,而且绑定了不同的类型,这很容易让人混淆,但实际上它的规则是很简单的。让我们先来谈谈 new,它是一个用来分配内存的内置函数,但是它不像其它语言中的 new 函数,它不初始化内存, new(T) 只是创建一个没有任何数据的类型为T的实例,并且返回它的地址——*T类型的值,在Go的术语中,它返回一个新建的分配了0空间的类型为T的实例的指针。

由于new返回的内存是归零了的,所以它能让我们很方便的设计它的数据结构,因为归零的数据可以不需要更多的初始货即可直接使用,比如 bytes.Buffer的文档这么写着“Buffer的归零数据是一个空的可以随便人到中年来使用的缓冲”。相反的,sync.Mutex没有明确的构建或者初始化方法,转而它的归零值为一个未锁定的mutex。

The zero-value-is-useful property works transitively. Consider this type declaration.

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer类型的值在其分配了或者仅仅只是声明了之后都是可以仍的,下面这段代码不需要更多的安排即可以正确的工作:

p := new(SyncedBuffer)  // type *Synceduffer
var v SyncedBuffer  // type SyncedBuffer

构造函数与复合声明(Constructors and composite literals)

很多时候一个归零的值并不足以满足程序的业务需求,这时一个初始化构造函数就成了必须的东西了,比如下面这段从 os 衍生的函数:

func NewFile(fd ind, name string) *File{
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

上面这样的写法有点太过于复杂和麻烦了,使用初始化成员可以更简单的完成这个工作,每次只需要使用一个表达式即可创建一个新实例:

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File(fd, name, nil, 0}
    return &f
}
注意:不像 C,返回本地变量的地址在Go中是完全可以的,当函数返回之后,本地变量地址所占用的存储空间同样的会生存下来,事实上,我们可以直接可以将上面函数中的最后两人行合并为一行:
return &File(fd, name, nil, 0}

上面这些初始化成员必须按其构造函数定义了的顺序赋值,这是很麻烦的一件事情,我们必须要一直记得它们是一个什么样的顺序,即使有100个,我们也必须记住这100个的顺序,好在我们可以使用一种更加合理且方便的方法来使用它,那就是为每一个初始化成员指定一个标签,其格式为field: value:

return &File(fd: fd, name: name}

如果初始化成员没有任何元素的话,那么就会创建一个归零的实例,这个时候,&File{}与new(File)是等价的。

复合声明同样可以被用来创建array、slice、map,通过指定适当的索引和map键来标识字段,在下面这个例子中,无论是Enone、Eio还是Einval初始化都能很好的工作,只要确保他们不同就好了。

a := [...]string    {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string       {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

用 make 分配内存

回到内存分配。内奸函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建 slice、map和channel,并且返回一个有初始值(非零)的T类型的实例,而不是 *T。从本质上讲,导致这三个类型有所不同的原因在于指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量这三项的描述符,在这些项目被初始化之前,slice为nil,对于slice、map和channel,make初始化了内部的数据结构,填充适应的值。

make 返回初始化后的(非零)值

例如,*make([]int, 10, 100)分配了100个整数的数组,然后用长度10和容易100创建了slice结构指向数组的前10个元素,区别是new([]int)返回指向新分配的内存的指针,而零值填充的slice鸨是指向nil的slice值。

下面这个例子展示这两人者之间的不同:

var p *[]int = new([]int)   // 分配slice结构内存 : *p = nil
                // 已经可用
var v []int = make([]int, 100)  // v 指向一个新分配的有 100 个整数的数组

var p *[]int = new([]int)   // 不必要的复杂的例子
*p = make([]int, 10, 100)

v := make([]int, 100)       // 更常见

务必赢得 make 仅适用于 map、slice和channel,并且返回的不是指针,应当用new来获得特定的指针。

本文是 「Golang.org」的原文档翻译,出于本人有限的英语水平,如有不当,还请指正。

说明

本文档演示了一个最简单的 Go 包的开发过程以及一些 Go 命令行工具的使用,以及Go是如何架构、安装与搜索包的。

代码组织

GOPATH 以及工作区

Go 语言设计的目标之一是让软件开发更简单,得到的结果是 Go命令不使用任何的 Makefiles或者其它的配置文件,取而代之的是直接使用程序的源代码来管理依赖关系及架构条件,这意味着,你程序的源代码与架构脚本总是同步的,因为它们就是同样的东西(注:如果使用 Makefiles之类的东西,那么,你源代码修改了,还需要再去修改Makefiles,而是在Go中,因为没有这些配置文件,所以,你修改源代码,同时直接就修改了这些配置)。

你唯一需要做的是设置一个 GOPATH 环境变量,GOPATH 的作用是告诉 Go 从哪里去搜索Go的执行脚本以及将你的Go安装到哪里.$GOPATH 是一个包括很多路径的列表,它的格式与系统的PATH变量格式一样,一个Unix系统中常见的GOPATH像这样的:

GOPATH=/home/user/ext:/home/user/mygo

(在Windows系统中,将“:”修改为“;”)

任何一个被定义至 $GOPATH$ 环境变量中的路径都指定着一个工作区的的位置(比如上例中的 /home/user/ext或者/home/user/mygo),一个工作区包含着Go源代码以及它们所依赖的包对象以及一些可执行的工具,它被规定为需要有下面这三个子目录:

  1. src : Go 源代码文件
  2. pkg : 以编译的 Go 包
  3. bin : 可执行命令

src 目录中的子目录都是独立的包,这些包中的所有源代码文件(.go,.co,.h以及.s等)都是子目录包的元素。

它构建一个import "widget"的程序时,Go构建工具会搜索 Go 根目录下的 src/pkg/widget 目录——如果没有找到,那么它会接着尝试去寻找同一工作区中的 src/widget 目录。

多个工作区可以给我们带来更我铁灵活性与方便,但是在本文档中,我们只涉及单个工作区,下面让我们先创建一个示例工作区,首先创建一个目录,我们用它来存放项目的源代码:

cox@CoxStation:~$ mkdir $HOME/example/src -p

下一步,设置 GOPATH 环境变量,你需要同时将 $GOPATH/bin 添加到 PATH 环境变量中,这样一来,你就不需要每次执行这些命令的时候输入完整的绝对路径了。

cox@CoxStation:~$ export GOPATH=$HOME/example
cox@CoxStation:~$ export PATH=$PATH:$HOME/example/bin

导入路径

Go 标准库或者包都可以使用最短的路径名导入,比如 fmt 或者 net/http 等,这是为了程序开发者的方便而设计的,但是对于你自己的项目,你需要认真的设计一个基本的导入路径,这需要考虑它它的可读性同时还需要考虑到它不应该和以有的或者将来会出现的其它的库相冲突。

最好的一个办法是总是以你所开发的项目的版本管理工具名称开始,比如,如果你的项目源代码库位置为 example.com 或者 code.google.com/p/example ,那么你应该让你的包路径从该 URL 地址开始,比如:example.com/foo/bar 或者 code.google.com/p/example/foo/bar ,如果你遵循这样的约定,那么 Go 会自动为你检测URL地址所指定的包,同时下载以及以你的导入路径安装至你的工作区中。

如果你没有像上面这样的发布路径,那么你至少应该使用一个唯一的前缀,比如 widget 以及 widget/foo/bar ,一个好的规则是使用你的项目名称或者公司名称作为前缀,这一般是不会被别人使用的,比如:example.com。

我们下面使用 example.com 作为基本的导入路径:

cox@CoxStation:~$ mkdir -p $GOPATH/src/example.com

包名称

任何一个 Go 源代码文件的第一个声明都应该是包名:

package NAME

NAME 就是你的包名称,它也是你导入该包时默认使用的名称(所有处于同一个包中的文件都必须共享同一个包名)。

Go 约定包名称是导入路径中的最后一个元素,一个以 crypto/rot12 导入的包的包名应该是 rot13,Go 不要求所有被导入到某一个程序中的包名都具有唯一性,但是要求其完整的导入路径必须是唯一的。

在 example 中创建一个新包,名为 newmath:

cox@CoxStation:~$ cd $GOPATH/src/example.com
cox@CoxStation:~/example/src/example.com$ mkdir newmath

之后创建一个名为 $GOPATH/src/example.com/newmath/sqrt.go 的文件,它的内容为:

// Package newmath is a trivial example package.
package newmath

// Sqrt returns an approximation to the square root of x.
func Sqrt(x float64) float64 {
    // This is a terrible implementation.
    // Real code should import "math" and use math.Sqrt.
    z := 0.0
    for i := 0; i < 1000; i++ {
        z -= (z*z - x) / (2 * x)
    }
    return z
}

该包将可以使用下面这个路径导入:

import "example.com/newmath"

构建以及安装

Go 命令包含了很多子命令,最核心的子命令就是install,运行go install importpath 将构建并安装它所依赖的所有包。

安装一个包 表示将构建之后的包文件或者可执行的文件写入到当前工作区的pkg或者bin目录中。

构建一个包

要构建并安装 newmath包,使用下面命令:

cox@CoxStation:~/example$ go install example.com/newmath

如果构建成功,那么该命令不会有任何输出。Go约定,如果没有指定导入路径,那么go使用当前目录,下面的这两行命令与上面那个是一样的效果:

$ cd $GOPATH/src/example.com/newmath
$ go install

构建之后的结果是go为你创建了一个目录树(假定我们现在使用的是Linux 64位系统——我就是这样的系统),它看起来像是这样的:

$GOPATH/
    pkg/
        linux_amd64/
            example.com/
                newmath.a
    src/
        example.com/
            newmath/
                sqrt.go

构建一个命令工具

Go 命令将包名为 main 的包视为可执行的,从而会将它安装到 $GOPATH/bin 目录中,创建一个名为 hello 的可执行命令,我们首先需要创建它的包,之后再创建包文件:

cox@CoxStation:~/example$ mkdir $GOPATH/src/example.com/hello
cox@CoxStation:~/example$ vi $GOPATH/src/example.com/hello/hello.go

文件内容为:

// Hello is a trivial example of a main package.
package main

import (
    "example.com/newmath"
    "fmt"
)

func main() {
    fmt.Printf("Hello, Golang. Sqrt(2) = %vn", newmath.Sqrt(2))
}

保存上面文件之后,我们运行下面这个命令安装它:

cox@CoxStation:~/example$ go install example.com/hello

之后你就可以像运行其它命令一样的运行我们的 hello 了:

cox@CoxStation:~/example$ $GOPATH/bin/hello
Hello, Golang. Sqrt(2) = 1.414213562373095

如果你将 $GOPATH/bin 目录添加到了全局环境变量 PATH 中的话,那么你还可以像下面这样的执行:

cox@CoxStation:~/example$ export PATH=$PATH:$HOME/example/bin/
cox@CoxStation:~/example$ hello
Hello, Golang. Sqrt(2) = 1.414213562373095

现在我们的示例程序的目录数看起来应该像下面这个样子的了:

$GOPATH/
    bin/
        hello
    pkg/
        linux_amd64/
            example.com/
                newmath.a
    src/
        example.com/
            hello/
                hello.go
            newmath/
                sqrt.go

测试

Go 内置了一个轻量级的测试框架,它由 go test 命令和 testing 包组成。

创建一个以 _test.go 结尾的文件,让它包含只有用来标记的参数 t *testing.T的名为TestFUNC* 的函数,那么测试框架就会执行每一个像上面这样的函数,其实函数名称中的 FUNC 修改成为你需要测试的函数名,如果该函数调用了失败函数,比如t.Error或者t.Fail,那么表示整个测试失败。

我们现在创建一个名为 $GOPATH/src/example.com/newmath/sqrt_test.go 的测试文件用来测试 Sqrt函数:

cox@CoxStation:~/example$ vi $GOPATH/src/example.com/newmath/sqrt_test.go

它的内容为:

package newmath

import "testing"

func TestSqrt(t *testing.T) {
    const in, out = 4, 2
    if x := Sqrt(in); x != out {
        t.Errorf("Sqrt(%v) = %v, want %v", in, x, out)
    }
}

现在运行测试:

cox@CoxStation:~/example$ go test example.com/newmath
ok      example.com/newmath 0.005s

远程包

一个导入路径可以告诉 go 如何从一个版本管理系统(比如Git或者Mercurial)中获取包代码,go命令使用这些属性自动从远程搜索包,比如我们的这个示例程序,它的远程地址是:code.google.com/p/go.example,如果你将该URL址址告诉go,那么go会自动从远程下载该包的源代码:

$ go get code.google.com/p/go.example/hello
$ $GOPATH/bin/hello
Hello, world. Sqrt(2) = 1.414213562373095

如果指定的远程包在本地没有复本,那么 go 会自动的为你下载它,如果已经存在,则直接跳过,运行了上面的命令之后,你的目录看起来应该像下面这样的了。

$GOPATH/
    bin/
        hello
    pkg/
        linux_amd64/
            code.google.com/p/go.example/
                newmath.a
            example/
                newmath.a
    src/
        code.google.com/p/go.example/
            hello/
                hello.go
            newmath/
                sqrt.go
                sqrt_test.go
        example/
            hello/
                hello.go
            newmath/
                sqrt.go
                sqrt_test.go

如果你不直接告诉 go 去下载安装某个包,而是在某一个你所写的包中导入它,比如:

import "code.google.com/p/go.example/newmath"

那么,go 一样的也会自动的为你做好这一切。

说明

  1. 本翻译文档中所涉及的代码和原始文档有些话差入。
  2. 你可以从这里下载到本文档中所创建的示例代码:http://dl.antusoft.com/examples/golang/go.example.tar.gz
  3. 官方示例项目地址为:http://code.google.com/p/go.example/

什么是Go?来自其网站的介绍:

Go 编程语言是一个使得程序员更加有效率的开源项目。Go 是有表 达力、简洁、清晰和有效率的。它的并行机制使其很容易编写多核 和网络应用,而新奇的类型系统允许构建有弹性的模块化程序。Go 编译到机器码非常快速,同时具有便利的垃圾回收和强大的运行 时反射。它是快速的、静态类型编译语言,但是感觉上是动态类型 的,解释型语言。Go 1 是 Go 语言的第一个稳定发布版本。

基本上任何一个语言最开始的学习的时候都离不开那个“Hello World!”程序,既然找不到其它的方法,那么,学习Go的时候还是也来一个Hello World吧。

package main

import "fmt"

/* 打印出 Hello World 的各种语言的版本 */

func main() {
    fmt.Printf("Hello, world; or 你好,世界n")
}

在上面的代码段中,首行是必须的,任何 Go 文件都必须以 package 开头,对于需要独立运行的执行文件,则必须是 package main;接着我们导入了 fmt 包,这就是说需要将 "fmt" 包加入 main,不是 main 的其它包都称之为库,其它许多语言中都有着类似的概念,比如Python中的 module。后面跟着的是一行注释。

最后面我们定义了一个名为 main() 的函数,当 Go程序在执行的时候,首先调用的函数就是 main.main(),这是从C语言中继承而来的,在这里我们使用关键字 func 定义了这个函数,这个函数不接受任何参数,也不返回任何类型的值,该函数将打印出 “Hello, world; or 你好,世界”字符串,你可以发现,在Go语言中,可以直接使用非ASCII字符,这是因为Go支持任何编码的字符串,你甚至可以使用非ASCII编码的字符作为变量或者函数名称。

将上面的代码保存为一个helloworld.go 文件,然后编辑它:

go build helloworld.go

这会在当前文件夹中生成一个同名的可执行文件,我现在使用的是Windows 7,所以,这个可执行文件的文件名为 helloworld.exe 。如果不想编译它即执行,可以使用 go run 命令:

go run helloworld.go

它会直接运行 helloworld 程序,而不是生成可执行文件后手工去执行。

直接进入正题,在任何一个地方创建一个名为 helloworld.go 的文件,然后立马执行它:

cox@Cox:~/workspace/go/src$ touch helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go
package :
helloworld.go:1:1: expected 'package', found 'EOF'

简单吧,看错误信息,你现在应该知道,没有绑定到某一个 package 的文件是不被 Go 认可的,所以,我们都需要加上这么一个,我们在前面的那个 helloworld.go 文件里面加上:

package main

之后再来执行:

cox@Cox:~/workspace/go/src$ vi helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go
# command-line-arguments
runtime.main: undefined: main.main

恩,Go程序需要从某一个位置开始执行,就像电脑有电源开关一样简单,在 Go 中,程序都是从一个名为 main 的函数开始执行,所以,再加上这个函数:

package main

func main() {}

再来执行它:

cox@Cox:~/workspace/go/src$ vi helloworld.go
cox@Cox:~/workspace/go/src$ go run helloworld.go

恩,没报任何错误了,那也就是说我们现在的程序已经正确运行,但是因为 main() 没有做任何事情,所以我们现在什么也看不到,我们现在让它来打印出 “Hello World! 您好!世界。” 这个字符串:

package main

func main() {
    Println("Hello World! 您好!世界。")
}

又出错了:

# command-line-arguments
./helloworld.go:4: undefined: Println

Println 未定义,恩,从我们上面的代码中确实看得到,从来没有它,突然就这么一下子就拿来用了,不成,就像你从来没有买过车,一辈子都没见过车,怎么可能走着走着就突然在开车了呢,我们来买一辆车:

package main

import "fmt"

func main() {
    fmt.Println("Hello World! 您好!世界。")
}

再来运行 helloworld.go:

cox@Cox:~/workspace/go/src$ go run helloworld.go
Hello World! 您好!世界。

成了,这个 fmt 就是我们买来的车,有了车我们才能开车,我们这里用了 fmt 里面的 Println 函数。

你学会了吗?

  • 任何一个 Go 文件都必须被关联到某一个包上
  • 任何一个 Go 程序都必须有一个开始点,就是那个 main 函数
  • 任何一个函数或者变量都必须被定义之后才能使用

Golang 是一个以类BSD许可发布的程序开发语言,官方提供有两个编译器 gc Go编译器和gccgo编译器,其中gccgo编译器是GNU C 编译器的一部分,而 gc 编译器比较成熟并且它进行了更多的测试,本文所使用的是 gc 编译器安装一个以二进制包发布的 Go 版本。

系统需求

gc 编译器可以在下面这些操作系统和架构上面运行,请确定您当前的系统满足这些需求,如果你所使用的系统不在当前列表中,那么你就应该试试 gcc 编译器安装,这有可能有适合您的系统。

操作系统名称 系统架构 说明
FreeBSD 7+ amd64,386 Debian GNU/kFreeBSD不被支持
Linux 2.6.23+(glibc) amd64,386,arm CentOS/RHEL 5.x不被支持,还无ARM版本的二进制包
Mac OS X 10.6/10.7 amd64,386 使用XCode的gcc(1)
Windows 2000+ amd64,386 使用mingw gcc(2),cygwin或msys不是必需的

1) 只有你想使用 cgo 时,gcc才是必须的;2) 你仅仅只需要安装命令行工具即可,如果你已经嬢了  Xcode 4.3+,那么你可以在组件面板中添加命令行工具

下载 Go Tools

访问 Go 项目下载页面:http://code.google.com/p/go/downloads/list,选择一个适合你的操作系统或者CPU架构的发行版,官方的发行版有FreeBSD,Linux,Mac OS X(Snow Leopard/Lion)以及Windows系统,同时支持 32位与64位CPU架构,如果没有适合你所使用的操作系统的二进制安装包,你可以需要从源代码安装或者使用gccgo而不是gc。

安装 Go Tools

Go的发行布将被安装至 /usr/local/go目录(Windows下为c:Go),但是你是可以修改该安装目录的,使用Go时,你需要指定 GOROOT 环境变量至你使用 Go 的目录,比如,如果你将 Go 安装至你的home目录,那么你需要在 $HOME/.profile文件中添加下面这两行:

export GOROOT=$HOME/go
export PATH=$PATH:$GOROOT/bin

Windows用户请设置环境变量,怎么设置我也不太明白,很少用那个东西……

FreeBSD, Linux以及Mac OS X压缩包

如果你是从一个老版本的Go进升级,那么你需要在安装之前先删除原先版本:

rm -r /usr/local/go

将你获得的压缩包文件解压至 /usr/local:

tar -C /usr/local -xvf go1.0.3.linux-amd64.tar.gz

包文件的名称可能和上面我所使用的不一样,因为我们的系统可能不一样,之后你可以将 /usr/local/go/bin目录添加至你的环境变更 PATH 中,比如添加至 /etc/profile或者$HOME/.profile。

Mac OS X 包安装工具

如果 Mac OS X 的包安装工具的话只需要接照安装工具的流程进行即可,默认会安装至 /usr/local/go 目录中。

Windows 下面自己研究

测试你的安装

要测试你的Go是否已经正确安装的最简单的办法就是写一个Go程序,比如,Hello Golang

package main

import "fmt"

func main() {
    fmt.Printf("Hello, Golang!n")
}

然后测试运行:

$ go run hello.go
Hello, Golang!

OK,这表示你安装成功了。

文本格式

Instiki 支持很多种不同的文本格式,每一个不同的 Web (一个Web可以简单的认为是一套系统上面可以建立多个站点中的一个)可以有自己不同的文本格式。

XHTML 友好的标记格式

对于任何一种使用 XHTML 友好标记格式的页面,系统都会将内容以 application/xhtml+xml 标记发送,这表示,你可以直接在页面插入 SVG 图片,如果你想了解更多关于 SVG 的知识,可以查看一下我的另一篇文章《可缩放矢量图形 - Scalable Vector Graphics (SVG)》,所有下面这些标记格式都是基于 Maruku以及它内置的增强版本的 Markdown 语法

  • Markdown+itex2MML这是Instiki默认使用的文本格式,它使用了 itex2MML ,并且允许你直接在文本中书法 itex 方程式 ,这些方程式会被 MathML 翻译为浏览器可识别的标签,而对于某些不支持MathML的浏览器,MathJax将被用来渲染 MathML。在某些数学公式使用很多页面中可能加载会十分的慢,但是 MathJax 是一个跨浏览器的工具。
  • Markdown+BlahTex/PNG这种文本格式会将你的数学公式编译为 PNG 图片,而不使用 MathML,安装会稍稍复杂一点,而且渲染的效果也没有 MathML的好,但是用户不需要使用支持 MathML 的浏览器即可查看。
  • Markdown如果你不需要数学公式的支持,那么选择这种方式是最好的,这样 $ 这个符号就没有什么特殊的含义了,在前面的两人种格式中,它被用来限定数学公式的范围。

HTML 友好的标记格式

如果你选择了这些格式里面的任何一种,那么内容将被标记为 text/html ,这表示你的网页里面不会有数学公式以及SVG图片,除非必要,我个人不建议你使用这些标记格式。

  • Textile使用 RedCloth 渲染 Textile 文本
  • RDoc支持 RDoc
  • Mixed与 Textile 类似,只是对于一个单独的分行,它不会被解释为 * <br /> *

分类

Instiki可以对所有页面进行归类,要给某个页面加入到一个分类中,只需要在页面中添加下面这一行即可:

category: food

你可以为一个页面添加多个分类,只需要在上面的那一行中写上多个分类即可,分类之间使用英文逗号分开:

category: food,restaurants

你可以将分类放在任何一个位置,但是我们约定将它放在文本的最下方,你还可以将一个页面归入某个类中但是在页面展示时不将其显示出来,只需要下面这样即可:

:category: S5-slideshow

上面这个示例将会让文档成为一个 S5 slideshow 页面.

文件上传

如果你选中了 Allow uploads… 的复选框,那么用户就可以上传文件至服务器,上传的方法是:

  1. 编辑某个需要插入图片的页面,在需要插入图片的地方插入下面这样的代码:
    [[mypic.jpg:pic]]

    或者:
    [[myfile.pdf:file]]

    注意 : 这里面填写的文件名称将是文件上传之后在服务器上面的名称,它并不需要与用户要地的文件名称一样。
  2. 保存页面,上面的代码会生成一个链接,并提示这里需要上传图片,点击上传链接即可打开文件上传页面。
  3. 在文件上传页面中,可以输入一个 Description :
+ 对于图片文件,它将是 &lt;image&gt; 标签的 *alt* 属性
+ 对于其它文件,它将作为提示文本,并且同样也是文件链接文本
  1. 当你上传完成之后,图片、视频、文件等会自动的出现在你的页面上。

如果你想为图片或者文件定义与 Description 不一样的文本,你可以使用下面这样的标记:

[[mypic.jpg|alt text:pic]]

或者:

[[myfile.pdf|link text:file]]

除了上面这种维基的格式,你同样还可以使用 Markdown 风格的标记方式:

![alt text](/mywiki/files/mypic.jpg)

或者:

[link text](/mywiki/files/myfile.pdf)

Wolfram CDF 文件

Wolfram Research 定义了一种文件格式名为 Camputable Document Format,这种文件需要浏览器插件的支持,如果你在文本中输入了像下面这样的标记:

[[Voronoi.cdf| 588 x 380 :cdf]]

那么会像 FlashPlayer一样,Instiki为会指向CDF文件的链接创建一个类似Flash获取插件的链接,它像下面这样的:

http://www.wolfram.com/cdf-player/

上面这个标记中,尺寸是可选的。

文件的管理

你可以在管理员界面中查看所有上传至维基的文件,并且可以直接删除他们,但是这里还有一个更快捷的方法,将:

[[myfile.pdf:file]]

修改为:

[[myfile.pdf:delete]]

保存页面之后,会生成一个:

*Delete myfile.pdf*

的链接,点击该链接,然后输入Web密码,即可删除该文件,这种方法可以被用来替换页面中已有的文件:

  1. 首先像上面这样删除该文件。
  2. 重新添加该页面,再加上下面这行标记:[[myfile.pdf:file]]
  3. 保存页面后作前面说过的上传文件的操作。

搜索

每一个页面顶部的搜索框除了支持普通搜索外,你还可以在这里面输入正则表达式,如果你懂正则,那么这个搜索工具将会帮助你更快的检索到你真正想要的内容,默认的,该搜索工具是大小写不敏感的,关于正则的有关知识,我这里不做任何的讲解,如果你想了解它,建议你从这里开始:http://www.regular-expressions.info/quickstart.html,下面只是几个示例:

  • (bzr|bazaar)
  • init[^w]
  • wikis?word

快捷访问键

绝大多数浏览器都支持通过键盘访问网页中的内容,我们称这为快捷键或者访问键,英文名称为AccessKeys,这些快捷键是由网页中的标签定义的,根据你所使用的操作系统或者浏览器的不同,可以通过下面这些方式访问快捷键:

  • Windows ⇒ Alt + AccessKey
  • Macintosh ⇒ Control + AccessKey
  • Opera 浏览器 ⇒ 先按 Shift+ESC ⇒ 然后按 AccessKey
  • GNOME 下的 FireFox ⇒ Alt + Shift + AccessKey

这里有一点需要注意,在Windows的Internet Explorer(IE)浏览器中,如果某个被设置了快捷链的链接指向另一个页面,那么当你击活该快捷键后,只会将焦点移动到该快捷键所定义的链接上,并不会自动的为你转向该链接所指向的页面,你需要再按一次回车键才能算作是点击。

在Instiki中,定义了下面这些快捷键:

  • Alt-E :编辑当前页面
  • Alt-U :查看当前版本
  • Alt-H :回到首页
  • Alt-A :查看所有页面列表
  • Alt-S :保存当前正在编辑的页面
  • Alt-C :显示所有隐藏的分类;退出当前页面的编辑模式
  • Alt-B :查看上一个版本
  • Alt-F :查看下一个版本
  • Alt-X :导出Wiki
  • Alt-W :编辑Web(当前站点设置)

结语

本文并未对 Instiki 的所有功能及使用方法都作了介绍,仅仅只是对其基本功能的一个说明,我会在更久的使用了它更了解它之后,对其它的一些功能做更深入的说明,比如它的 SVG-EDIT 编辑器(这是一个开源项目,你可以在你的项目中使用),数学公式以及我很喜欢的那个 S3 Slideshow等等。

最近一直在为 Golang Wiki 选择维基引擎,最开始选择的是 DokuWiki 与 MoinMoinWiki ,前者是因为它足够的简单,后者是因为个人对 Python 的喜爱,然后,在网上找寻的过程中知道了 Instiki Wiki Engine,基于Ruby,它让我眼前一亮的是内置的 SVG编辑器 以及 Slideshow功能 ,尤其是前者,太爽了,所以,现在也在自己的本地测试与试用这个系统了。

简单的特性介绍 - Introduction to Instiki

Instiki 提供了基于 itex2MML 的对数学公式的支持,但是要求文本格式为系统默认的 “Markdown + itex2MML” 格式。同时,在每一个维基页面底部都有一个名为 “TeX” 的链接,用户可以通过它将内容导出为 LaTex 格式,不过这个我还没有测试过导出来的文档是不是对中文也很友好。

最让我眼前一亮的就是 “SVG编辑器以及直接将SVG图片插入内容中“ 的功能,它使使得我们可以在编辑内容的时候直接打开一个SVG图片编辑器,完成图片编辑之后,可以选择保存至本地图片库或者远程图片库(这需要提供相关图片库的帐号),更简单的,是可以直接将编辑后的图片插入文本内容中,因为SVG可以使用纯文本描述,所以我们根据就不需要将它保存为文件,如果选择直接插入SVG图片代码,我们还是可以再修改它的,办法就是把整个SVG代码选中,创建SVG图片的按钮就会变成编辑已有图片,点击它就会载入选中的图片进行编辑。

另外一个很不错的功能(但是有可能我会用不着),就是对数据公式的支持(可以说是完美的支持),它并不将我们的公式转为图片,而是使用的MathJax库,以文本的方式展示公式,这个有机会使用得到的时候再好好的去研究一下下。

一个 Instiki 实例可以创建无数个维基站点(在 Instiki 中称之为 “Webs”),每一个站点又都可以被设置为密码保护(使用独立的密码),如果你愿意,还可以将一个密码保护的站点发布一份“只读(Read-Only)”版权,这样其它人可以读取它的内容但是无法对其做出任何修改。

安装与系统需求 - Install & Requirements

在我写这篇文章时,它要求Ruby版本为 1.8.6、1.8.7或者1.9.2,Rubygems版本为 1.3.6或者更高的版本。

Mac OS X - Leopard, Snow Leopard 以及 Lion

Ruby 1.8.7 以及 Rubygems 是跟随开发者工具套件(Developer Tools) 一起被安装的,该套件现在已经可以在 App Store 上面直接下载到,只需要你有一个 Apple ID 即可,在苹果电脑上面安装 Instiki 需要该套件的支持,如果你没有安装,需要先安装它,这根据你的网速可能需要一些时间,尤其是在国内网络情况并不太理想的前提下。

首先将 Rubygems 升级到最新版本:

sudo gem update --system
sudo gem update

如果你的 Rubygems 版本过老的话,可能上面的命令无法成功执行,这需要你手工升级它,详情请移步这里:http://rubygems.org/pages/download ,之后你就可以安装 Instiki 了,关于如何在 Tiger 上面的安装 Instiki ,请移步这里:http://golem.ph.utexas.edu/wiki/instiki/show/Installing+under+MacOSX+Tiger

Linux

在 Debian 或者 Ubuntu 上面安装Instiki,你可以执行下面这行命令安装一些需求包(我安装的时候因为被SVG编辑器完全吸引了,而忘记了安装说明,最后是自己一次一次出错才成功安装的,每出一次错补装一个需求包):

$ sudo apt-get install ruby ruby1.8-dev libopenssl-ruby rake rubygems libsqlite3-ruby1.8 ri1.8 libxslt-dev libxml2-dev libsqlite3-dev swig flex bison

如果你的机器上面都有这些了,那就没必须再安装了,我的电脑上面就是没有 libxslt-ruby 、libsqlite3-dev 以及 libxml2-dev 。

如果你使用的是 Febora 的话,则可以执行下面这行命令来完成需求包的安装:

$ yum install make ruby ruby-devel rubygems sqlite sqlite-devel swig flex bison

然后还需要执行下面这行命令来创建一个软链接:

$ ln -s /usr/lib/libsqlite3.so.0.8.6  /usr/lib/libsqlite3.so
注意 :如果你得到下面这样一个错误信息:

install_gem_spec_stubs': undefined method `loaded_specs' for Gem:Module (NoMethodError)

那表示你的 Rubygems 版本过低,你可以通过下面这两人行命令升级它:

sudo gem install rubygems-update
   sudo update_rubygems

CentOS 是一个 RedHat 家族的Linux发行版,所以,你可以直接像 Fedora 一样做,只是有已知的有两个不一样的错误可能会发成在CentOS上面:

  1. /dev/null is not world-writable ,你需要:
    chmod 777 /dev/null
  2. /dev/urandom is not a "special character device.” ,你需要:
    rm /dev/urandom
    mknod -m 644 /dev/urandom c 1 9
    chown root:root /dev/urandom

对于这上面这个解决办法,你可能需要写进自启动脚本中,因为我们现在所作的任何修改,在系统重新启动之后都会失效。

Windows 系列

饿……没什么想法,我从来不认为这些软件应该被安装在Windows 系统之上,所以,如果你真的有这样的需求,自己去找解决办法吧……

安装 Instiki - Install Instiki

从 http://rubyforge.org/frs/?group_id=186 下载 Instiki 的最新版并解压,如果你想试用最新的开发版本,则可以下载 http://golem.ph.utexas.edu/~distler/code/instiki-svn.tar.gz ,不过开发中的版本可能存在很多的Bug,但是Instiki一直使用的都是开发中的版本,所以,如果有Bug的话,应该也是能在第一时间得到解决的。

如果你选择开发版,你还可以使用 bzr :

bzr branch http://golem.ph.utexas.edu/~distler/code/instiki/svn/ location-of-Instiki/

或者使用 Git :

git clone http://github.com/parasew/instiki.git location-of-Instiki/

之后如果要更新开发版本,只需要使用 [bzr pull]golem.ph.utexas.edu/wiki/instiki/show/Bazaar 或者git pull 即可。

注意 :安装时请讲上面两人行命令中的 location-of-Instiki/ 修改成为你要安装它的目录,同时保证整个路径中没有空格。

运行 Bundler 并启动你的 Instiki 之旅 - Running Bundler and starting Instiki

如果上面的安装或者取得文档都没有错误发生,那么可以在你的 Instiki 主目录中运行下面这行命令了:

ruby bundle install --path vendor/bundle

这将下载并安装其它的一些需求包(sqlite3-ruby/itextomml等),最后:

./instiki --daemon

将启动 Instiki。

打开你的浏览器,并在地址栏中输入:http://localhost:2500 并设置你的第一个维基站点,这很简单,只需要填写维基的名称以及路径和管理它的密码即可。

维护 - Care and Feeding

你可以通过下面这行命令停止 Instiki

% kill pid-of-Instiki

然后 Instiki 还提供了很多的命令行工具用来维护与保养你的 Instiki。

运行:

./instiki

将使用 Mongrel(如果你安装了的话)或者 WEBrick 启动 Instiki,如果你已经安装了 Mongrel,但是却还是想通过 WEBrick 启动 Instiki的话,需要给上面的命令加上 webrick 参数:

./instiki webrick

但是这样的方式启动的 Instiki 将直接在前端运行,所以当你退出终端或者键入 Ctrl + C 之后就会立马停止,如果你想在后端运行,那么你需要像前面那样运行:

./instiki --daemon

或者:

./instiki webrick --daemon

下面还有一些其它的命令行标记:

  • -d, –daemon :以守护进程的方式运行 Instiki
  • -p, –port=Port :定义Instiki监听的端口,默认端口号为 2500
  • -b, –binding=1p :绑定 Instiki 到特定的地址,默认监听 0.0.0.0,即监听所有地址
  • -c, –config=file :使用一个特定的配置文件
  • -u, –debugger :启用Debug模式
  • -e, –environment=name :设定服务器运行的模式(test/development/production),默认为产品模式,即 production
  • -P, –path=/path :绑定一个URL路径给Instiki,默认为 /
  • -h, –help :显示帮助信息,并列出可用的命令

为 Instiki 优化的主机服务 - Dreamhost

Dreamhost 总是为这些开源软件进行各种各样的优化,同样的,在DreamHost上面安装并运行 Instiki 也是十分简单的一件事情,DreamHost 通过 mod_rails 对 Ruby-on-Rails* 提供支持,唯一不足是它只提供了老版本的 sqlite3。

  1. 在DreamHost上面创建一个新的域名虚拟主机(比如 wiki.matrixstack.com)
  2. 在 Specify your web directory 选项中选中 Ruby on Rails Passenger(mod_rails) 选项,并填写路径wiki.matrixstack.com/public

这会在你的家目录中创建一个名为 wiki.matrixstack.com 的目录,登陆 Shell 然后执行下面这些命令:

cd wiki.yourdomain.com
wget http://golem.ph.utexas.edu/~distler/code/instiki-svn.tar.gz
tar --strip-components 1 -xzf instiki-svn.tar.gz
sed '/sqlite3-ruby", :r/s//sqlite3-ruby", "~>1.2.0", :r/' Gemfile > Gemfile.tmp
mv Gemfile.tmp Gemfile
unset GEM_HOME GEM_PATH
ruby bundle

打开浏览器并访问 http://wiki.matrixstack.com对维基进行设置即可。

升级已有的 Instiki 安装

Instiki 的开发还处于活跃期,经常会有新功能被添加或者Bug被修补,所以你肯定时常想升级你现有的Instiki安装,如果你通过 Bazaar 安装,那么:

  1. 运行
    % bzr pull

    或者
    % git pull
  2. 进行所有需要的升级
    % ruby bundle update
    % ruby bundle exec rake upgrade_instiki
  3. 重新启动 Instiki
    % ps -ax | grep instiki
    % kill PID-of-Instiki
    % ./instiki --daemon

如果你是通过 tarball 安装的,那么:

  1. 备份所有上传的文件,它们被保存在 webs/Name-Of-Web/files/ 目录中
  2. 如果你使用默认的 SQLite3 数据库,则需要备份它,它位于 db/production.db.sqlite3 ,如果你使用的是MySQL,则需要备份 config/database.yml 文件
  3. 停止 Instiki
    % ps -ax | grep instiki
    % kill PID-of-Instiki
  4. 下载并解压最新版本
  5. 将第 1 与第 2 步备份的文件重新恢复到对应的目录中
  6. 运行
    % ruby bundle update
    % ruby bundle exec rake upgrade_instiki
  7. 重新启动 Instiki
    % ./instiki --daemon

完成 Instiki 的升级。

安全相关 - Security

软件再好也不可能达到完美的安全,要不然也不会有人站出来说:“永远不要相信用户的输入”了。

以非特权用户运行 Instiki

如果你是一个具有服务器特权的用户,那么我不建议你直接以该用户的身份运行 Instiki,你可以创建一个新的无特权的用户来运行:

  1. 创建一个无特权的没有 shell 访问权限的新用户,我这里就创建一个 instiki 用户
  2. 让该用户仅仅只能访问以下这些必须的文件:
    % sudo chown instiki public secret db db/production.db.sqlite3 config/environment.rb config/database.yml
    % sudo chown -R instiki log storage cache webs tmp
  3. 以 instiki 用户的身份运行 Instiki:
    % sudo -u instiki ./instiki --daemon
  4. 在这种情况下,你可以通过下面这种方式停止 Instiki
    % sudo -u instiki kill pid-of-Instiki

使用 MySQL 数据库替换默认的 SQLite

如果你使用 Sqlite 已经运行你的Instiki一段时间,现在计划转到 MySQL 数据库上去,那么你可以这样做:

  1. 确定你现在运行的Instiki版本大于 0.19,然后执行下面这行命令:
    % ruby bundle exec rake upgrade_instiki

    这会升级你的数据库Schema到最新版本,老版本的数据库Schame可能导致在SQLite3⇒MySQL的转换过程中数据的丢失。
  2. 在 Gemfile 文件的最后面添加下行这一行:
    gem "mysql"

    然后重新执行:
    % ruby bundle

    这将为你安装好 Mysql Gem
  3. 在你的系统环境变量中添加一个新的变量:
- 如果你使用的是 sh ( bash, zsh 等):

        % RAILS_ENV='production'

- 如果你使用的是 csh (tcsh 等):

        % setenv RAILS_ENV production
  1. 导出你的文件至 dump/fixtures/.yml*
    % ruby bundle exec rake db:fixtures:export_all
  2. 编辑 config/database.yml ,并修改:
    production:
       adapter: sqlite3
       database: db/production.db.sqlite3

    为:
    production:
      adapter: mysql
      database: your_db_name
      username: your_db_username
      password: your_db_password
      host: 127.0.0.1
      port: 3306
  3. 创建一个新的MySQL数据库,数据库信息与你上面填写的一保持一致:
    % echo "create database your_db_name" | mysql -u your_db_username -p your_db_password
  4. 初始化你的数据库表并导入你的数据:
    % ruby bundle exec rake db:migrate
    % ruby bundle exec rake db:fixtures:import_all

大小写敏感问题

这里可能还有一个小问题需要你注意一下下,那就是在 SQLite3中,字符串是大小写敏感的,这也就是说你可以有一个页面的名称为 Foo ,而另一个页面的名称为 foo ,这使得 Instiki 会将它们分开对待,但是在 MySQL 下面,这个正好是相反的,字符串默认是大小写不敏感的。

这并没有哪一种方法可以说是绝对正确的,只们只是对待事物的方式不一样而已,如果你想在MySQl数据库中也对页面的名称大小写敏感,你可以单独的通过SQL命令修改受该问题影响的字段,根据你自己数据库的字符集编码选择下面命令中的任何一种即可:

ALTER TABLE `pages` MODIFY `name` varchar(255) COLLATE latin1_bin DEFAULT NULL;
ALTER TABLE `wiki_references` MODIFY `referenced_name` varchar(255) COLLATE latin1_bin NOT NULL DEFAULT '';
ALTER TABLE `wiki_files` MODIFY `file_name` varchar(255) COLLATE latin1_bin NOT NULL;

或者

ALTER TABLE `pages` MODIFY `name` varchar(255) COLLATE utf8_bin DEFAULT NULL;
ALTER TABLE `wiki_references` MODIFY `referenced_name` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '';
ALTER TABLE `wiki_files` MODIFY `file_name` varchar(255) COLLATE utf8_bin NOT NULL;

这个问题我只在使用 Ubuntu 系统的时候出现过,因为很久没有使用其它系统了,所以也不知道是不是都是一样的,会遇到这种问题,这个问题发生的原因是因为系统默认没有开启 sudo 权限,解决办法很简单:

pantao@aliyun:~$ sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup
[sudo] password for pantao:
pantao is not in the sudoers file.  This incident will be reported.
pantao@aliyun:~$ su
Password:
root@aliyun:/home/pantao# chmod u+w /etc/sudoers
root@aliyun:/home/pantao# vi /etc/sudoers
root@aliyun:/home/pantao# chmod u-w /etc/sudoers
root@aliyun:/home/pantao# exit
exitsudo ap
pantao@aliyun:~$ sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup
[sudo] password for pantao:
pantao@aliyun:~$

在上面的代码中,最开始我是要备份软件源,但是因为我不在 sudoers 文件中,所以系统不允许我在 /etc/ 目录下面执行写操作,然后运行 su ,输入 root 用户密码之后切换到超级用户,然后:

  1. 修改 /etc/sudoers 文件权限,让所有者可写
  2. 打开该文件,并在 root ALL=(ALL:ALL) ALL 这一行下面添加:
    pantao  ALL=(ALL) ALL

    请将 pantap 修改成为你自己的用户名
  3. 按 ESC 键退出编辑状态,再输入 :wq 保存修改并退出编辑器
  4. 去掉该文件的写权限
  5. 退出 root 帐户
  6. 现在已经可以正常使用 sudo 了。

最终修改后的文件片段如下:

[...]
# Cmnd alias specification

# User privilege specification
root    ALL=(ALL:ALL) ALL
pantao  ALL=(ALL) ALL
# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL
[...]

Beautiful Soup 是一个用来从HTML与XML文档中抓取数据的Python库,它提供了从HTML与XML文档中检索、抓取与修改数据的最傻瓜式的方法,能为我们的开发节省很多时间,本文翻译自 Beautiful Soup Documentation,但并非完全地逐字翻译,如果你想查阅原版英文文档,请点击前面的链接,本文绝大多数示例都是在Python的命令行工具中进行的。

功能演示与快速入门

下面这个摘录自《爱丽丝仙境梦游记》的故事段落是我们最开始要进行处理的HTML文档:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

上面的代码并没有进行合理的缩进,那是因为我最开始想像大家展示一下 Beautiful Soup提供给我们的第一个有用的功能——美化代码结构:

>>> from bs4 import BeautifulSoup
>>> soup = BeautifulSoup(html_doc)
>>> print(soup.prettify())
<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
 <body>
  <p>
   <b>
    The Dormouse's story
   </b>
  </p>
  <p>
   Once upon a time there were three little sisters; and their names were
   <a href="http://example.com/elsie" id="link1">
    Elsie
   </a>
   ,
   <a href="http://example.com/lacie" id="link2">
    Lacie
   </a>
   and
   <a href="http://example.com/tillie" id="link3">
    Tillie
   </a>
   ;
and they lived at the bottom of a well.
  </p>
  <p>
   ...
  </p>
 </body>
</html>

下面的这些示例则展示了如何通过Beautiful Soup获取HTML文档中的数据:

>>> soup.title
<title>The Dormouse's story</title>
>>> soup.title.name
'title'
>>> soup.title.string
u"The Dormouse's story"
>>> soup.title.parent.name
'head'
>>> soup.p
<p><b>The Dormouse's story</b></p>
>>> soup.p['class']
['title']
>>> soup.a
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.find(id='link3')
<a href="http://example.com/tillie" id="link3">Tillie</a>

还有一个我们用得最多的功能那就是从一个网页中获取所有网址,来看看Beautiful Soup做这个事情有多简单:

>>> for link in soup.find_all('a'): print(link.get('href'))
...
http://example.com/elsie
http://example.com/lacie
http://example.com/tillie

另一个用得最多的功能就是从某一个元素中获取文本内容:

>>> print(soup.get_text())
The Dormouse's storyThe Dormouse's story
Once upon a time there were three little sisters; and their names were
Elsie,
Lacie and
Tillie;
and they lived at the bottom of a well.
...
>>> print(soup.p.get_text())
The Dormouse's story

安装 Beautiful Soup

看了上面的这些功能演示是不是已经心动了,心动不如行动,先安装Beautiful Soup吧。

如果你使用的是Debian或者Ubuntu Linux,你可以使用系统的包管理工具直接安装:

$ apt-get install python-bs4

Beautiful Soup 4以在PyPi中发布了,所以,如果你不能使用系统的包管理工具安装它,那也可以使用Python的安装工具 easy_install 或者 pip 安装,在PyPi中的包名称为 beautifulsoup4,该包能同时工作于 Python 2.x 与 Python 3.x。

$ easy_install beautifulsoup4

或者

$ pip install beautifulsoup4
如果你使用的某一个软件是基于 Beautiful Soup 3 的,那么你可能需要了解一下怎么安装它,查阅:http://www.crummy.com/software/BeautifulSoup/bs3/documentation.html以获得更多的帮助信息,但是如果你是准备基于Beautiful Soup 创建新的应用,那么我强烈建议你使用 Beautiful Soup 4。

如果你的机器上面连 easy_install 或者 pip 工具都没有,那你还可以直接下载安装包使用包里的setup.py 安装,下载地址为:http://www.crummy.com/software/BeautifulSoup/download/4.x/,下载完成之后,将其解压至某个目录下,运行:

$ python setup.py install

如果还是安装失败,那要么你可以选择检查是哪里出了问题,再尝试安装,或者,最简单的办法,Beautiful Soup 4所使用的许可证允许你将它的所有代码直接包装进你自己的应用中,这样就不再需要安装它了。

Beautiful Soup 4是在 Python2.7与Python3.2上开发的,但是它应该是可以运行在其它版本中的。

安装之后的一些问题

Beautiful Soup包本身是 Python 2 的代码,当你安装至Python 3之后,它会自动转换成为Python 3代码,但是如果你不安装该包,代码则不会被转换,在Windows机器上会报出你安装了错误的版本。

  • 如果你得到 ImportError “No module named HTMLParser” ,则表示你在 Python 3中运行Python 2版本的代码。
  • 如果你得到 ImportError “No module named html.parser” ,则表示你在 Python 2中运行Python 3版本的代码。
  • 如果上面两个错误你都有,那最好的办法是删除所有现有安装,然后重新安装一次试试。
  • 如果你得到 SyntaxError “Invalid syntax” on the line ROOT_TAG_NAME = u'[document]' ,你需要转换Python 2的代码至Python 3代码,你可以通过安装脚本来转换: $ python3 setup.py install

或者使用Python 的 2to3 转换脚本进行:

$ 2to3-3.2 -w bs4

安装解析器

Beautiful Soup 支持Python标准库中的HTML的解析器,但是你同样还可以使用很多其它的第三方解析器,比如 lxml parser 。根据你的系统不同,你可以使用下面中的某一种方式安装 lxml:

$ apt-get install python-lxml
$ easy_install lxml
$ pip install lxml

如果你使用 Python 2,另一个纯Python的HTML解析器 html5lib parser 也是很不错的选择,它可以像浏览器一样的解析HTML文档,用下面的任何一种方法安装它:

$ apt-get install python-html5lib
$ easy_install html5lib
$ pip install html5lib

下表简单地说明了每一种解析器优点与缺点:

解析器 典型用法 优点 缺点
Python标准库 html.parser BeautifulSoup(markup, “html.parser”) 内置,不错的速度,宽容(2.7.3 与 3.2 ) 不是太宽容
lxml HTML parser BeautifulSoup(markup, “lxml”) 非常的快 , 宽容 依赖于外部C库
lxml XML parser BeautifulSoup(markup, [“lxml”, “xml”]) , BeautifulSoup(markup, “xml”) 非常的快,只支持正确的XML文档 依赖于第三方C库
html5lib BeautifulSoup(markup, html5lib) 宽容,像浏览器一样解析页面,操作HTML5文档 非常慢,依赖第三方库,仅支持 Python 2

如果可以的话,我建议你安装 lxml 库,如果你使用的是 2.7.3或者3.2.2以前的版本,建议你使用 lxml 或者 html5lib,早期版本的Python的HTML Parser并不是非常的好。

注意:不同的解析器会为其一份文档创建不同的 tree 。

创建 Soup

要解析一个文档,你可以将该文档的所有字符串或者一个打开的文件对象传送给BeautifulSoup构造器:

from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html'))

soup = BeautifulSoup('<html>data</html>')

首先,文档会被转换为Unicode编码,HTML 元素也会被转换为Unicode字符串。

>>> BeautifulSoup("这是中文")
<html><body><p>这是中文</p></body></html>

Beautiful Soup 接着使用一个可用的解析器分析文档,如果不是特别指定的话,它会默认使用一个HTML解析器。

对象类型

Beautiful Soup 将HTML文档转换成为一个 Python 对象树,但是你只需要处理这下面这四种类型的对象即可。

标签 (Tag)

一个 Tag 对象对应于 XML 或 HTML 文档中的某一个标签:

>>> soup = BeautifulSoup('<b>Extremely bold</b>')
>>> tag = soup.b
>>> type(tag)
<class 'bs4.element.Tag'>

Tag对象有许多的方法与属性可供我们使用,这些会在本文下方专门的章节做介绍。

名称(Name)

任何一个Tag都有一个名称,通过 .name 属性访问:

>>> tag.name
'b'

如果你修改了它的名称,那么此次修改会体现到任何一个由Beautiful Soup 创建的HTML元素上:

>>> tag.name = 'blockquote'
>>> tag
<blockquote>Extremely bold</blockquote>

属性(Attributes)

一个Tag可以有不限数量的属性,如 <b class=“boldest”> 就有一个名为 class 的属性,它的值为boldest ,你可以像字典一样访问Tag的属性,比如:

>>> tag['class']
['boldest']

如果你想获取整个Tag的所有属性的字典,可以使用 .attrs

>>> tag.attrs
{'class': ['boldest']}

你可以添加、删除或者修改Tag的属性:

>>> tag['class'] = 'verybold'
>>> tag['id'] = 1
>>> tag
<blockquote id="1">Extremely bold</blockquote>

>>> del tag['class']
>>> del tag['id']
>>> tag
<blockquote>Extremely bold</blockquote>

多值的属性

HTML 4 定义了一些可以有多个值的属性,HTML 5又移除了一些,但是最后却又定义了更多的新的,最常见的可多值的属性是 class ,其它的还有比如 rel , rev , accept-charset , headers 以及accesskey 等,Beautiful Soup将多值以列表(list)的方式进行处理:

>>> css_soup = BeautifulSoup('<p>Strikeout</p>')
>>> css_soup.p['class']
['body', 'strikeout']
>>> css_soup = BeautifulSoup('<p>Strikeout</p>')
>>> css_soup.p['class']
['body']

如果一个属性,在文档中看似是一个多值的,但是却没有在任何一个HTML标准中被定义为多值属性,则 Beautiful Soup 会按标准执行,即将其按单个值进行处理:

>>> id_soup = BeautifulSoup('<p id="my id"></p>')
>>> id_soup.p['id']
'my id'

当Tag重新转为HTML代码时,多个值会被合并:

>>> rel_soup = BeautifulSoup('<p>Back to the <a rel="index">Home Page</a></p>')
>>> rel_soup.a['rel']
['index']
>>> rel_soup.a['rel'] = ['index', 'contents']
>>> print(rel_soup.p)
<p>Back to the <a rel="index contents">Home Page</a></p>

但是如果你使用的是 xml 解析器,那么不存在多值属性:

>>> xml_soup = BeautifulSoup('<p></p>', 'xml')
>>> xml_soup.p['class']
u'body strikeout'

可操纵字符串(NavigableString)

Beautiful Soup 将 Tag 中的字符串内容称之为 NavigableString :

>>> tag.string
u'Extremely bold'
>>> type(tag.string)
<class 'bs4.element.NavigableString'>

NavigableString 与 Unicode 字符类型,当然它还提供有很多特殊的功能,这些我也会在下面的章节中作详细介绍,你可以使用 unicode() 将其转换为 Unicode 字符:

>>> unicode_string = unicode(tag.string)
>>> unicode_string
u'Extremely bold'
>>> type(unicode_string)
<type 'unicode'>

你不能直接编辑字符,但是你可以使用一个新字符替换它:

>>> tag.string.replace_with("No langer bold")
u'Extremely bold'
>>> tag
<blockquote>No langer bold</blockquote>

BeautifulSoup 对象

BeautifulSoup 对象则是文档本身,很多时候你可以像Tag的那样操作它,但是因为它并不是一个真正的Tag,所以它没有名称,也没有属性,但是我们经常需要获取它的名称,所以,它有一个名称,为 [document]

>>> soup.name
u'[document]'

注释以及其它一些字符

Tag , NavigableString 以及 BeautifulSoup 提供了几乎所有我们在HTML或者XML文档中可见的内容,但是还有一些特殊的内容则没有被它们包括在里面,比如你最常需要考虑的是其注释:

>>> markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
>>> soup = BeautifulSoup(markup)
>>> comment = soup.b.string
>>> type(comment)
<class 'bs4.element.Comment'>

Comment 仅仅只是一个特殊的 NavigableString :

>>> comment
u'Hey, buddy. Want to buy a used parser?'

当一个 Comment 对象以 HTML 元素出现的时候,它有自己的特殊格式:

>>> print(soup.b.prettify())
<b>
 <!--Hey, buddy. Want to buy a used parser?-->
</b>

Beautiful Soup 同样定义了 XML 文档中会出现的各种各样的元素,比如 CData ,ProcessingInstruction , Declaration 以及 Doctype 等,像 Comment 一样,所有这些类都是NavigableString 的子类,但是都有它们自己的特性,下面这是一个 CData 示例:

>>> from bs4 import CData
>>> cdata = CData("A CDATA block")
>>> comment.replace_with(cdata)
u'Hey, buddy. Want to buy a used parser?'
>>> print(soup.b.prettify())
<b>
 <![CDATA[A CDATA block]]>
</b>

浏览树对象 (Navigating the tree)

“三姐妹”的HTML文档又来了:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

我还是将使用这个文档来演示如何在树对象中游走。

往内在子元素中检索(Going Down)

Tag 可以包含字符串或者其它 Tag,这些被包裹的元素称为 Tag 的子元素,Beautiful Soup 提供了很多属性用来在 Tag 的子元素中进行导航或者遍历子元素。

注意:Beautiful Soup 字符串没有上面说的这些任何一种属性,因为字符串无法包含子元素。

使用 Tag 名称

最简单的检索树的方法是使用标签名称,如果你想获取 <head> 标签 ,只需要: soup.head :

>>> soup.head
<head><title>The Dormouse's story</title></head>
>>> soup.title
<title>The Dormouse's story</title>

你可以像上面这一样一直往后加添加标签的名称,比如我想获取 <body> 中的 <b> ,则可以使用 soup.body.b :

>>> soup.body.b
<b>The Dormouse's story</b>

使用标签浏览的局限在于我们总是只能得到第一个为该名称的子标签:

>>> soup.a
<a href="http://example.com/elsie" id="link1">Elsie</a>

如果你想得到所有某一个名称的标签,你需要使用搜索树的方法,比如 find_all() :

>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

.contents 与 .children

标签的所有子元素可以通过一个名为 .contents 的列表访问到:

>>> head_tag = soup.head
>>> head_tag
<head><title>The Dormouse's story</title></head>
>>> head_tag.contents
[<title>The Dormouse's story</title>]
>>> title_tag = head_tag.contents[0]
>>> title_tag
<title>The Dormouse's story</title>
>>> title_tag.contents
[u"The Dormouse's story"]

BeautifulSoup 对象只有一个子元素,即 <html> 标签:

>>> len(soup.contents)
1
>>> soup.contents[0].name
'html'

字符串没有任何 .contents ,因为它无法包裹其它数据:

>>> text = title_tag.contents[0]
>>> text.contents
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/bs4/element.py", line 667, in __getattr__
    self.__class__.__name__, attr))
AttributeError: 'NavigableString' object has no attribute 'contents'

除了通过列表 .contents 来访问子元素外,你还可能通过遍历访问它们,使用 .children :

>>> for child in title_tag.children: print(child)
...
The Dormouse's story

.descendants

descendants 是后代的意思,它不像 .contents 或者 .children 只能访问到父元素的下一级子元素,它能访问到父元素内的所有子元素,比如:

>>> head_tag.contents
[<title>The Dormouse's story</title>]

我们只得到了一个子元素,但是:

>>> for child in head_tag.descendants: print(child)
...
<title>The Dormouse's story</title>
The Dormouse's story

可以看到,当得到的子元素有其自己的子元素,那么也会一并的遍历到,这是因为 <head> 虽然只有一个子元素 <title> ,但是 <title> 也有一个子元素,这个子元素同样是 <head> 的后代:

>>> len(list(soup.children))
1
>>> len(list(soup.descendants))
23

.string

如果一个标签有且只有一个子元素,而该子元素是一个 NavigableString ,那么可以直接使用 .string访问到这个子元素:

>>> title_tag.string
u"The Dormouse's story"

如果一个元素有且仅有一个子元素,而该子元素同样有且仅有一个子元素并且该子元素是一个NavigableString ,那么该字符同样是最开始父元素的 .string :

>>> head_tag.contents
[<title>The Dormouse's story</title>]
>>> head_tag.string
u"The Dormouse's story"

但是如果一个元素有多个字符的话,因为我们无法通过一个属性来指定多个值,所以我们直接将该元素的 .string 设定为 None

>>> print(soup.html.string)
None

.strings 与 stripped_strings

像上面说过的,我们无法使用 .string 名称指定多个值,但是却可以使用 .strings 访问到,它是一个生成器(Generator):

>>> for string in soup.strings: print(repr(string))
...
u"The Dormouse's story"
u"The Dormouse's story"
u'n'
u'Once upon a time there were three little sisters; and their names weren'
u'Elsie'
u',n'
u'Lacie'
u' andn'
u'Tillie'
u';nand they lived at the bottom of a well.'
u'n'
u'...'

我在上面的示例中使用了 repr 函数是因为我想告诉你 .strings 会将任何字符串都囊括在内,这在很多时候是不需要的,所以还提供了另一个生成器 .stripped_strings :

>>> for string in soup.stripped_strings: print(repr(string))
...
u"The Dormouse's story"
u"The Dormouse's story"
u'Once upon a time there were three little sisters; and their names were'
u'Elsie'
u','
u'Lacie'
u'and'
u'Tillie'
u';nand they lived at the bottom of a well.'
u'...'

所有空字符串包括只有换行符等等都被去除掉了。

向外在父元素中检索(Going Up)

就像“族谱”一样,任何一个标签一定有它的父元素:

.parent

你可以访问通过 .parent 访问一个标签的父元素,在上面的示例HTML文档中, <head> <title> 的父元素:

>>> title_tag.parent
<head><title>The Dormouse's story</title></head>

顶层的元素的父元素就像Linux系统中的根目录 / 一样,就是 BeautifulSoup 对象本身:

>>> html_tag = soup.html
>>> type(html_tag.parent)
<class 'bs4.BeautifulSoup'>

而 BeautifulSoup 对象的父元素则被定义为 None :

>>> print(soup.parent)
None

.parents

就像 .descendants 可以遍历到一个元素的所有子元素一样,你还可以使用 .parents 遍历一个元素的所有父元素:

>>> link = soup.a
>>> link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> for parent in link.parents:
...     if parent is None:
...         print(parent)
...     else:
...         print(parent.name)
...
p
body
html
[document]

同等级兄弟元素中平行检索(Going Sideways)

考虑一下下面这个HTML文档:

>>> sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")
>>> print(sibling_soup.prettify())
<html>
 <body>
  <a>
   <b>
    text1
   </b>
   <c>
    text2
   </c>
  </a>
 </body>
</html>

<b> 与 <c> 是具有同一个父元素的同一级标签,我们称这两个元素为兄弟元素,它们处于文档结构中的同一级,它们之间的这种关系同样可以被用来进行内容的检索。

.next_sibling 与 .previous_sibling

你可以使用 .next_sibling 与 .previous_sibling 检索下一个与上一个元素:

>>> sibling_soup.b.next_sibling
<c>text2</c>
>>> sibling_soup.c.previous_sibling
<b>text1</b>

因为 .next_sibling 与 .previous_sibling 都只是在同一级的兄弟元素中检索,所以,<b> 有 .next_sibling 但是因为它前面没有兄弟元素,所以它的 .previous_sibling 为 None,而 <c> 有 .previous_sibling 但是后面没有兄弟元素,所以它的 .next_sibling 为 None:

>>> print(sibling_soup.b.previous_sibling)
None
>>> print(sibling_soup.c.next_sibling)
None

“text1” 与 “text2” 却不是兄弟元素,因为他们没有同一个父元素:

>>> sibling_soup.b.string
u'text1'
>>> print(sibling_soup.b.string.next_sibling)
None

在真实的文档中, .next_sibling 与 .previous_sibling 得到的一般总是一个空白字符串,回到前面三姐妹的那个示例HTML文档中:

<a href="http://example.com/elsie" id="link1">Elsie</a>
<a href="http://example.com/lacie" id="link2">Lacie</a>
<a href="http://example.com/tillie" id="link3">Tillie</a>

你可以会认为第一个 <a> 的下一个元素是 Lacie,但是并不是这样的:

>>> link = soup.a
>>> link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> link.next_sibling
u',n'
>>> link.next_sibling.next_sibling
<a href="http://example.com/lacie" id="link2">Lacie</a>

这是因为空白也是一个子元素,空白字符串不代表,没有。

.next_siblings 与 .previous_siblings

你可以使用 .next_siblings 与 .previous_siblings 来遍历某一个元素所有前面的兄弟元素或者所有它后面的兄弟元素:

>>> for sibling in soup.a.next_siblings: print(repr(sibling))
...
u',n'
<a href="http://example.com/lacie" id="link2">Lacie</a>
u' andn'
<a href="http://example.com/tillie" id="link3">Tillie</a>
u';nand they lived at the bottom of a well.'
>>> for sibling in soup.find(id='link3').previous_siblings: print(repr(sibling))
...
u' andn'
<a href="http://example.com/lacie" id="link2">Lacie</a>
u',n'
<a href="http://example.com/elsie" id="link1">Elsie</a>
u'Once upon a time there were three little sisters; and their names weren'

根据文档解析器的解析流程进行检索(Going back and forth)

返回来看看最开始的那个 三姐妹 的示例:

<html><head><title>The Dormouse's story</title></head>
<p><b>The Dormouse's story</b></p>

HTML 解析器对上面这段HTML文档的解析可以以一个一个的事件来表示:

  1. 打开一个 <html> 标签
  2. 打开一个 <head> 标签
  3. 打开一个 <title> 标签
  4. 添加一个字符串 “The Dormouse's story”
  5. 关闭 <title> 标签
  6. 关闭 <head> 标签
  7. 打开一个 <p> 标签
  8. ……

BeautifulSoup 提供工具可以让你根据这种流程对文档进行检索

.next_element 与 .previous_element

.next_element 属性指向当前元素的下一个元素,看上去它与 .next_sibling 似乎有些一样,但是其实是完全不同的两码子事儿,比如在三姐妹的示例中, 最后一个 <a> 的 .next_sibling 是一个字符串:

>>> last_a = soup.find('a', id = 'link3')
>>> last_a
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_a.next_sibling
u';nand they lived at the bottom of a well.'

但是 .next_element 却不一样,根据上面所说的章节的说明, 它的值应该是 Tillie 这个词:

>>> last_a.next_element
u'Tillie'

简单来说,next_sibling 或者 previous_sibling 是根据标签或者元素在文档中的最终位置来定义的,但是 .next_element 与 previous_element 则是根据元素的解析流程来定义的。

.previous_element 与 .next_element 正好相反,它表示在当前元素前面被解析的元素:

>>> last_a.previous_element
u' andn'
>>> last_a.previous_element.next_element
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_a
<a href="http://example.com/tillie" id="link3">Tillie</a>

.next_elements 与 .previous_elements

到现在为止你应该找到规律了吧, .next_elements 与 .previous_elements 检索当前元素的所有后面被解析的以及前面被解析的元素:

>>> for element in last_a.next_elements: print(repr(element))
...
u'Tillie'
u';nand they lived at the bottom of a well.'
u'n'
<p>...</p>
u'...'

在树中搜索(Searching the tree)

Beautiful Soup 定义了许多用来搜索解析树的方法,但是所有这些方法都差不多,我将着重花时间来解释两人个使用频率最多的方法 find() 与 find_all() ,其它的方法基本上都是使用同样的参数定义,所以我将对其它的只作简单的介绍。

同样的,我们还是使用三姐妹的示例:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p><b>The Dormouse's story</b></p>

<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p>...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

通过传递一个过滤器到某一个方法(比如 find_all() ),你可以获取到文档中的任何元素。

过滤器类型(Kinds of filters)

在对 find_all() 或者其它相类似的方法进行详细的说明前,我将先向你展示可以传递给这些方法的不同的过滤器类型,这些过滤器在整个搜索API中一次又一次的出现,你可以基于一个标签的名称,或者它的属性,或者一个文本字符串等等创建过滤器。

一个字符串

我简单的过滤器就是一个字符串,通过传递字符串到搜索方法中,Beautiful Soup将返回所有包含该字符的标签的元素:

>>> soup.find_all('b')
[<b>The Dormouse's story</b>]
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

如果你传递的是一个 byte string, Beautiful Soup 会先将其编码为 UTF-8 字符串,所以你可以直接传递Unicode 字符串。

一个正则表达式

如果你传递一个正则表达式对象, Beautiful Soup 将使用它的 match() 方法对文档进行过滤,下面这段代码将搜索文档中所有以 b 开头的标签的元素:

>>> import re
>>> for tag in soup.find_all(re.compile('^b')): print(tag.name)
...
body
b

下面这段示例代码则搜索所有标签名称中包含 t 的元素:

>>> for tag in soup.find_all(re.compile('t')): print(tag.name)
...
html
title

一个列表

如果你传递的是一个列表,那么 Beautiful Soup 会返回“符合其中任何一个过滤器要求的”元素,下面这段代码将搜索到所有的 <b> 与 <a> 标签:

>>> soup.find_all(['a','b'])
[<b>The Dormouse's story</b>, <a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

True

True 将匹配文档中的所有标签,但是不包括字符串内容:

>>> for tag in soup.find_all(True): print(tag.name)
...
html
head
title
body
p
b
p
a
a
a
p

一个函数

如果上面的所有过滤器中没有一个能满足你的要求,那么你还可以提供自己的过滤函数,该函数需要接收一个标签作为参数,而且只允许接收这一个参数,它应该返回 True 或者 FalseTrue 表示匹配, False 表示不匹配。

下面这个示例函数将匹配所有具有 class 属性但是不没有 id 属性的元素:

>>> def has_class_but_no_id(tag):
...     return tag.has_key('class') and not tag.has_key('id')
...

我们使用这个函数对当前文档进行一些搜索过滤:

>>> soup.find_all(has_class_but_no_id)
[<p><b>The Dormouse's story</b></p>, <p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p>...</p>]
>>> for tag in soup.find_all(has_class_but_no_id): print(tag.name)
...
p
p
p

下面我们再定义一个示例函数,它将过滤出所有前后被字符串包裹的元素:

>>> from bs4 import NavigableString
>>> def surrounded_by_strings(tag):
...     return (isinstance(tag.next_element, NavigableString) and isinstance(tag.previous_element, NavigableString))
...
>>> for tag in soup.find_all(surrounded_by_strings): print tag.name
...
p
a
a
a
p

find_all()

函数签名: find_all(name, attrs, recursive, text, limit, kwargs)

find_all() 方法是根据标签的 “ 父-子 ” 关系对文档进行检索,并返回当前匹配过滤器的所有当前元素的子元素,在前面的一些示例中我已经使用过这个函数了,这里再列出一些更多该函数的示例:

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.find_all('p', 'title')
[<p><b>The Dormouse's story</b></p>]
>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.find_all(id='link2')
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
>>> import re
>>> soup.find(text=re.compile('sisters'))
u'Once upon a time there were three little sisters; and their names weren'

上面这些示例中,有一些与我之前使用的是一样的,但是有一些却是刚刚才使用的新的,比如 text与 id 这两人个参数分别都表示什么?为什么 find_all('p','title') 返回一个 class 为 title 的 <p> 元素?下面让我们来详细的看看传递给 find_all() 方法的参数。

name 参数

传递一个 name 参数,代表你告诉 Beautiful Soup 你只需要名称为参数所指定的标签,文本字符串以及未匹配名称的标签都将直接忽略。

简单的用法示例:

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]

关键字参数

如果一个函数未定义的关键字参数被传递给 find_all() 方法,那么该关键字的键将作为标签的属性进行搜索,而匹配规则是标签的该属性为该关键字参数的值,所以,当我们传递 id=“link2” 时,会匹配到 id 属性值为 link2 的元素:

>>> soup.find_all(id="link2")
[<a href="http://example.com/lacie" id="link2">Lacie</a>]

你可以使用一个字符串、正则表达式、列表、一个函数或者一个 True 值来过滤,比如下面的代码找到所有设定了 id 属性的元素:

>>> soup.find_all(id=True)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

同样的,你还可以一次性通过多个属性对元素进行过滤:

>>> soup.find_all(href = re.compile('elsie'), id = 'link1' )
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

以 CSS 类为参数搜索

因为 class 是Python的一个关键字,所以我们无法以上面关键字参数的方式通过 class 属性过滤,但是我们有一个替代方案,那就是使用 class_ :

>>> soup.find_all('a', class_='sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

这个时候 class_ 就与前面的关键字参数无异,你同样可以使用一个字符、正则表达式等等的作为该参数的值:

>>> soup.find_all(class_=re.compile('itl'))
[<p><b>The Dormouse's story</b></p>]
>>> def has_six_characters(css_class):
...     return css_class is not None and len(css_class) == 6
...
>>> soup.find_all(class_=has_six_characters)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
注意: class 是一个可多值的属性,当匹配任何其中一个值是,都会算成功匹配,如下示例代码:
>>> css_soup = BeautifulSoup('<p></p>')
>>> css_soup.find_all('p', class_='strikeout')
[<p></p>]
>>> css_soup.find_all('p', class_='body')
[<p></p>]

你也同样可以将完整的 class 值传递给 find_all() :

>>> css_soup.find_all('p', class_='body strikeout')
[<p></p>]

但是对于上例中,如果将两个类返过来写就不能匹配了:

>>> css_soup.find_all('p', class_='strikeout body')
[]

这是很不方便的一件事情,如果我们要通过 class 来搜索元素,这样的方法还需要 class 名称的顺序是统一的,但是我们可以写一个函数:

>>> css_soup = BeautifulSoup('<p></p><p>Another One</p>')
>>> css_soup.find_all('p', class_='body strikeout')
[<p></p>]
>>> css_soup.find_all('p', class_='strikeout body')
[<p>Another One</p>]

对于 class ,Beautiful Soup 还提供了一个快捷方式,那就是 attrs ,传递一个字符串给 attrs 将以 class 对元素进行过滤:

>>> soup.find_all('a', 'sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

你同样也可以传递一个正则表达式、函数或者 True,等参数,但是不允许使用字典类型的数据,这些都和使用 class_ 关键字的效果是一样的。

>>> soup.find_all('p', re.compile('itl'))
[<p><b>The Dormouse's story</b></p>]

如果传递的是一个字典的话,它不再仅仅只是当作 class 搜索了,而是以字典中的键为属性名称,值为属性值进行搜索:

>>> soup.find_all(href=re.compile('elsie'), id = 'link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.find_all(attrs={'href': re.compile('elsie'), 'id': 'link1'})
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

使用 text 参数

除了对属性进行过滤外,还可以通过文本内容对元素进行过滤,使用的关键字就是 text ,你可以传递一个字符串、正则表达式、列表、函数或者一个 True 值,下面是一些简单的示例:

>>> soup.find_all(text='Elsie')
[u'Elsie']
>>> soup.find_all(text=['Tillie', 'Elsie', 'Lacie'])
[u'Elsie', u'Lacie', u'Tillie']
>>> soup.find_all(text=re.compile('Dormouse'))
[u"The Dormouse's story", u"The Dormouse's story"]
>>> def is_the_only_string_within_a_tag(s):
...     '''Return True if this string is the only child of its parent tag.'''
...     return (s == s.parent.string)
...
>>> soup.find_all(text=is_the_only_string_within_a_tag)
[u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

虽然 text 是用来搜索字符串的,但是你可以通过绑定一个标签名称来搜索标签, Beautiful Soup 会搜索所有 .string 值匹配你所指定的字符串的元素:

>>> soup.find_all('a', text='Elsie')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

使用 limit 参数

limit 参数可以限定返回的结果集元素的数量,当一个文档十分大的时候,这个参数将十分有用,它所做的工作就像SQL中的 LIMIT 一样,在我们的示例文档中,有三个链接,但是我现在想将结果限定为两个:

>>> soup.find_all('a', limit=2)
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>]

使用 recursive 参数

通常的,当你从一个标签中呼叫 find_all() 方法时,它会搜索该函数的子元素,以及子元素的子元素……但是如果你将 recursive 参数设置为 False ,它将只匹配它自己的第一级子元素,而子元素的子元素则直接跳过:

>>> soup.html.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.html.find_all('title', recursive = False)
[]

下面这是我们示例的HTML代码片段:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
...

<title> 虽然是 <html> 标签的子元素,但是并不是它的直属子元素,所以我们将搜索不到。

Beautiful Soup 提供了很多基于树的搜索方法,这些方法基本上都使用与 find_all() 同样的参数,唯一不同的是 recursive 参数只能被使用在 find_all() 与 find() 这两人个方法中,其它的方法都不支持该参数。

像呼叫 find_all() 一样呼叫一个标签

因为 find_all() 是我们使用得多的 Beautiful Soup 搜索API,所以这里还提供了一个呼叫该方法的快捷方式,如果你将一个 Beautiful Soup 对象或者一个 Tag 对象当作一个函数来对待,那么你得到的结果将和使用该对象呼叫 find_all() 方法得到的结果一样:

>>> soup.find_all('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup('a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

下面这两人个也是一样的:

>>> soup.title.find_all(text=True)
[u"The Dormouse's story"]
>>> soup.title(text=True)
[u"The Dormouse's story"]

find()

函数签名: find(name, attrs, recursive, text, kwargs)

find_all() 搜索呼叫它的对象的所有内容,但是有时候你仅仅只想搜索一个结果,如果你知道整个HTML文档只有一个 <body> 元素,那么搜索整个文档完全是一种浪费,除了每一次你都指定一个limit=1 这个参数外,你还可以使用一个更简单的方法: find() 。

>>> soup.find_all('title')
[<title>The Dormouse's story</title>]
>>> soup.find('title')
<title>The Dormouse's story</title>

这两者之间唯一的区别是 find_a;;() 返回一个结果集(不管结果集中有多少元素),而 find() 只返回匹配到的一个结果,如果未找到任何结果,那么 find_all() 会返回一个空的列表,但是 find() 则返回None。

>>> print(soup.find_all('nothing'))
[]
>>> print(soup.find('nothing'))
None

如果你还记得 soup.head.title 这种方式的检索的话,那么你应该发现,这两者之间得到的结果是一样的,这是因为它本身就是调用的 find() 接口:

>>> soup.head.title
<title>The Dormouse's story</title>
>>> soup.find('head').find('title')
<title>The Dormouse's story</title>

find_parents() 与 find_parent()

函数签名: find_parents(name, attrs, text, limit, kwargs)

函数签名: find_parent(name, attrs, text, kwargs)

在本文的前面部分已经有很大的篇幅来介绍 find_all() 与 find() 两人个函数了,下面还有十个函数是 Beautiful Soup 提供的搜索API,五个类似于 find_all() ,另外五个类似于 find() 。这些函数之间的唯一区别是他们搜索的范围不同。

首先让我们来看看 find_parents() 与 find_parent() :

>>> a_string.find_parents('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>]
>>> a_string.find_parents('p')
[<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;

and they lived at the bottom of a well.

] >>> len(a_string.find_parents('p')) 1 >>> print(a_string.find_parents('p', 'title')) []

 

因为 <p> 与 三个 <a> 中的某一个是 a_string 的父元素,所以我们都能搜索得到结果,但是因为 class 为 title 的 <p> 元素并不是a_string的父元素,所以我们这里得不到结果,而 find_parent() 则类似 find() 仅仅只返回一个结果。

>>> a_string.find_parent()
<a href="http://example.com/lacie" id="link2">Lacie</a>
>>> a_string.find_parent('p')
<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

你可能已经与我们前面说过的 .parent 与 .parents 属性联系上的,这两人个属性与我们刚才所了解的find_parent() 与 find_parents() 的关联是很紧密的,这两人个函数本质上就是使用这两个属性进行遍历。

find_next_siblings() 与 find_next_sibling()

函数签名: find_next_siblings(name, attrs, text, limit, kwargs)

函数签名: find_next_sibling(name, attrs, text, kwargs)

这两个函数对 .next_siblings 生成器进行遍历以搜索:

>>> first_link = soup.a
>>> first_link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> first_link.find_next_siblings('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> first_link.find_next_sibling('a')
<a href="http://example.com/lacie" id="link2">Lacie</a>

find_previous_siblings() 与 find_previous_sibling()

函数签名: find_previous_siblings(name, attrs, text, limit, kwargs)

函数签名: find_previous_sibling(name, attrs, text, kwargs)

这两个函数对 .previous_siblings 进行遍历以搜索:

>>> last_link = soup.find('a', id='link3')
>>> last_link
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_link.find_previous_siblings('a')
[<a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> last_link.find_previous_sibling('a')
<a href="http://example.com/lacie" id="link2">Lacie</a>

find_all_next() 与 find_next()

函数签名: find_all_next(name, attrs, text, limit, kwargs)

函数签名: find_next(name, attrs, text, kwargs)

这两个函数对 .next_elements 进行遍历以搜索:

>>> first_link = soup.a
>>> first_link
<a href="http://example.com/elsie" id="link1">Elsie</a>
>>> first_link.find_all_next(text=True)
[u'Elsie', u',n', u'Lacie', u' andn', u'Tillie', u';nand they lived at the bottom of a well.', u'n', u'...']
>>> first_link.find_next('p')
<p>...</p>

find_all_previous() 与 find_previous()

函数签名: find_all_previous(name, attrs, text, limit, kwargs)

函数签名: find_previous(name, attrs, text, kwargs)

这两个函数对 .next_previous 进行遍历以搜索:

>>> last_link = soup.find('a', id='link3')
>>> last_link
<a href="http://example.com/tillie" id="link3">Tillie</a>
>>> last_link.find_all_previous('p')
[<p>Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" id="link1">Elsie</a>,
<a href="http://example.com/lacie" id="link2">Lacie</a> and
<a href="http://example.com/tillie" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>, <p><b>The Dormouse's story</b></p>]
>>> last_link.find_previous('title')
<title>The Dormouse's story</title>

CSS 选择器

CSS 选择器是一个十分强大的工具,Beautiful Soup 也提供了 CSS selector standard 定义的很多选择器,你仅仅只需要将选择器作为一个字符串传递给 .select() 即可:

你可以搜索标签:

>>> soup.select("title")
[<title>The Dormouse's story</title>]

或者标签中所有的子元素标签:

>>> soup.select('body a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者搜索某一个标签的下一层标签:

>>> soup.select('head > title')
[<title>The Dormouse's story</title>]
>>> soup.select('p > a')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('body > a')
[]

通过 CSS class 搜索标签:

>>> soup.select('.sister')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

>>> soup.select('[class~=sister]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者通过ID搜索:

>>> soup.select('#link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.select('a#link1')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

通过检测一个标签的属性是否存在搜索:

>>> soup.select('a[href]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]

或者通过指定标签属性的值来搜索:

>>> soup.select('a[href="http://example.com/elsie"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]
>>> soup.select('a[href^="http://example.com/"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>, <a href="http://example.com/lacie" id="link2">Lacie</a>, <a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('a[href$="tillie"]')
[<a href="http://example.com/tillie" id="link3">Tillie</a>]
>>> soup.select('a[href*=".com/el"]')
[<a href="http://example.com/elsie" id="link1">Elsie</a>]

匹配语言代码:

>>> multilingual_markup = """
...  <p lang="en">Hello</p>
...  <p lang="en-us">Howdy, y'all</p>
...  <p lang="en-gb">Pip-pip, old fruit</p>
...  <p lang="fr">Bonjour mes amis</p>
... """
>>> multilingual_soup = BeautifulSoup(multilingual_markup)
>>> multilingual_soup.select('p[lang|=en]')
[<p lang="en">Hello</p>, <p lang="en-us">Howdy, y'all</p>, <p lang="en-gb">Pip-pip, old fruit</p>]

该功能只要求用户有一定的CSS选择器知识即可使用。

修改树(Modifying the tree)

Beautiful Soup 的主要任务被设计为从现有文档中搜索与检索数据,但是你同样可以对文档的对象树进行修改并将其保存到一新的HTML或者XML文档中。

修改标签名称及属性

在本文的前面部分我就已经使用该功能做过示例,你可以重新为一个标签命名,修改一个标签的属性的值或者添加新属性以及删除已有数据等等:

>>> soup = BeautifulSoup('<b>Extremely bold</b>')
>>> tag = soup.b
>>> tag.name = 'blockquote'
>>> tag
<blockquote>Extremely bold</blockquote>
>>> tag['class'] = 'verybold'
>>> tag
<blockquote>Extremely bold</blockquote>
>>> tag['id'] = 1
>>> tag
<blockquote id="1">Extremely bold</blockquote>
>>> del tag['class']
>>> tag
<blockquote id="1">Extremely bold</blockquote>
>>> del tag['id']
>>> tag
<blockquote>Extremely bold</blockquote>

修改标签的 .string

如果你设置一个标签的 .string 属性,那么它的 contents 值将被修改为你设定的 .string 值:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.string = "New link text."
>>> tag
<a href="http://example.com/">New link text.</a>
注意:如果一个标签包含很多子标签,那么你使用这种方法修改 .string 值将会使用该值替代原来的所有内容,包括它的子标签等等。

追加- append()

你可以通过 Tag.append() 来添加标签的内容,它就像在Python List上使用 .append() 一样:

>>> soup = BeautifulSoup("<a>Foo</a>")
>>> soup.a.append("Bar")
>>> soup
<html><body><a>FooBar</a></body></html>
>>> soup.a.contents
[u'Foo', u'Bar']

BeautifulSoup.new_string() 与 .new_tag()

如果你需要向文档中添加字符串,你可以使用 append() 方法,或者你可以使用工厂方法BeautifulSoup.new_string() :

>>> soup = BeautifulSoup("<b></b>")
>>> tag = soup.b
>>> tag.append("Hello")
>>> new_string = soup.new_string(" there")
>>> tag.append(new_string)
>>> tag
<b>Hello there</b>
>>> tag.contents
[u'Hello', u' there']

如果你想插入一个完整的新的标签而不仅仅是往标签中插入内容,那最好的办法是使用BeautifulSoup.new_tag() 方法:

>>> soup = BeautifulSoup("<b></b>")
>>> original_tag = soup.b
>>> new_tag = soup.new_tag("a", href="http://www.example.com")
>>> original_tag.append(new_tag)
>>> original_tag
<b><a href="http://www.example.com"></a></b>
>>> new_tag.string = 'Link Text.'
>>> original_tag
<b><a href="http://www.example.com">Link Text.</a></b>

该方法仅仅只有第一个参数——标签名称是必须的。

插入- insert()

Tag.insert() 就像 Tag.append() 一样,不同在于它不仅仅只是将新标签添加到父元素 .contents 属性的最后,而可以将新标签添加到任何一个你所指定的位置,它就像 Python list中的 insert() 函数一样:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.insert(1, "but did not endorse ")
>>> tag
<a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
>>> tag.contents
[u'I linked to ', u'but did not endorse ', <i>example.com</i>]

在前面或者后面插入 - insert_before() 与 insert_after()

insert_before() 方法会在标签或者字符串的前面立马插入新内容:

>>> soup = BeautifulSoup("<b>stop</b>")
>>> tag = soup.new_tag("i")
>>> tag.string = "Don't"
>>> soup.b.string.insert_before(tag)
>>> soup.b
<b><i>Don't</i>stop</b>

insert_after() 则是在标签或字符的后面立马插入新内容:

>>> soup.b.i.insert_after(soup.new_string(" ever "))
>>> soup.b
<b><i>Don't</i> ever stop</b>
>>> soup.b.contents
[<i>Don't</i>, u' ever ', u'stop']

清除- clear()

Tag.clear() 移除一个标签的所有内容:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> tag = soup.a
>>> tag.clear()
>>> tag
<a href="http://example.com/"></a>

extract()

PageElement.extract() 将一个标签从对象树中完全移除,并返回被移除的标签本身:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> i_tag = soup.i.extract()
>>> a_tag
<a href="http://example.com/">I linked to </a>
>>> i_tag
<i>example.com</i>
>>> print(i_tag.parent)
None

现在你得到了两人个根对象 BeautifulSoup 对象和另一个 extract 出来的对象,你可以继续对这个对象使用同样的方法:

>>> my_string = i_tag.string.extract()
>>> my_string
u'example.com'
>>> print(my_string.parent)
None
>>> i_tag
<i></i>

decompose()

decompose() 方法将一个标签从树中移除,然后 完全的删除它以及它的内容 。

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> soup.i.decompose()
>>> a_tag
<a href="http://example.com/">I linked to </a>

replace_with()

PageElement.replace_with() 删除一个标签或者一个字符串,然后使用一个你指定的标签或者字符串代替原来被删除的标签或者字符的位置:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> new_tag = soup.new_tag("b")
>>> new_tag.string = "example.net"
>>> a_tag.i.replace_with(new_tag)
<i>example.com</i>
>>> a_tag
<a href="http://example.com/">I linked to <b>example.net</b></a>

replace_with() 返回被移除的标签或者字符,这使得你可以在稍后重新将其加入文档或者插入到其它地方去。

wrap()

PageElement.wrap() 将你给定的元素使用你指定的标签包裹,它返回新的容器:

>>> soup = BeautifulSoup("<p>I wish I was bold.</p>")
>>> soup.p.string.wrap(soup.new_tag("b"))
<b>I wish I was bold.</b>
>>> soup.p.wrap(soup.new_tag("div"))
<div><p><b>I wish I was bold.</b></p></div>

unwrap()

Tag.unwrap() 是 wrap() 的反向操作,它使用标签中的内容来替换标签本身:

>>> markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
>>> soup = BeautifulSoup(markup)
>>> a_tag = soup.a
>>> a_tag.i.unwrap()
<i></i>
>>> a_tag
<a href="http://example.com/">I linked to example.com</a>

与 replace_with() 一样,unwrap() 返回替换前的原始标签。

输出(Output)

美化后输出

prettify() 方法将一个 Beautiful Soup 解析树格式化为一个美化的 Unicode 字符,每一个标签将独占一行,并进行合理的缩进:

    >>> str(soup)
    '<html><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'

昨天晚上申请的一个新域名,其实是有一个好的网站的想法,发扬我想到就做的优良传统,立马就注册了这个相应的域名, codinuts.com 为 *coding nuts* 的全体词,意思是 “*我为编码狂*”, 网站程序准备使用刚刚开始接触的 Golang 进行开发,数据库现在还没有想好使用什么,我其实现在正好也在研究 Amazon AWS,所以,数据库准备使用 AWS RDS,静态文件准备使用AWS S3,而网站的VPS则使用AWS EC2,恩,是一个不错的搭配。

该网站的功能简单来说就是:用户提出实际问题,用户用编码的方式解决实际问题。任何一个实际问题都可以让任何用户来解决,他们可以将他的解决方法公布出来,而每一个解决方法又可以被别的用户优化。更具体的东西,等以后再慢慢地说, 这里只是记录一下下想法,如果你发现这篇文章的发布时间已经是很久以前了,不防看看 Codinuts.com 是否已经上线了……