2012年八月月 发布的文章

Go程序设计语言(三)

本文译自Rob PikeGo语言PPT教程 – "The Go Programming Language Part3(updated June 2011)"。由于该教程的最新更新时间早于Go 1版本发布,因此该PPT中的一些内容与Go 1语言规范略有差异,到时我会在相应的地方做上注解。

第三部分大纲

  • 并发与通信
    • Goroutines
    • 通道(Channel)
    • 并发相关话题

并发与通信:Goroutines

Goroutines

术语:

对于"并发运行的事物"已经有了好多术语 – 进程、线程、协程(coroutine)、POSIX线程、NPTL线程、轻量级进程…,但这些事物都或多或少有不同。并且Go中的并发与哪种都不甚相同。

因此我们介绍一个新术语:goroutine。

定义

一个Goroutine是一个与其他goroutines运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。

它与线程、协程、进程等不同。它是一个goroutine。

注意:Concurrency与Parallelism是不同的概念。如果你不了解它们的不同,查查相关资料吧。

关于并发的问题有许多。我们后续会提及。现在就假设它能按其对外所宣称的那样正常工作吧。

启动一个Goroutine

调用一个函数或方法,然后说go:

func IsReady(what string, minutes int64) {
    time.Sleep(minutes * 60*1e9) // Unit is nanosecs.
    fmt.Println(what, "is ready")
}
go IsReady("tea", 6)
go IsReady("coffee", 2)
fmt.Println("I'm waiting…")

打印:

I'm waiting… (立即)
coffee is ready (2分钟后)
tea is ready (6分钟后)

一些简单的事实

goroutine的使用代价很低。

当从最外层函数返回,或执行到结尾处时,goroutine退出。

goroutines可以并行地在不同CPU上执行,共享内存。

你无需担心栈大小。

在gccgo中,至少目前goroutines就是pthreads。在6g中,goroutines采用基于线程的多路复用技术,因此它们的代价更低廉。

无论是上面哪个实现,栈都很小(几KB),可以根据需要增长。因此goroutines使用很少的内存。你可以创建很多goroutines,它们还可以动态拥有很大的栈。

程序员无需考虑栈大小相关话题。在Go中,这种考虑甚至不应该出现。

调度

Goroutine多路复用系统线程。当一个goroutine执行了一个阻塞的系统调用时,其他goroutine不会不阻塞。

计划后续实现CPU绑定的goroutines,不过目前用6g如果你想要用户层级别的并行,你必须设置环境变量GOMAXPROCS或调用runtime.GOMAXPROCS(n)。

GOMAXPROCS告诉运行时调度器有多少个用户空间goroutine即将同时执行,理想情况下在不同的CPU核上。

*gccgo总是为每个goroutine单独分配一个线程执行。

并发与通信:Channels

Go中的Channel

除非两个goroutine可以通信,否则它们无法协作。

Go中有一个名为channel的类型,提供通信和同步能力。

Go中还提供一些特殊的基于channel的控制结构,使得编写并发程序更加容易。

Channel类型

该类型最简单形式:
    chan elementType

通过这个类型的值,你可以发送和接收elementType类型的元素。

Channel是引用类型,这意味着如果你将一个chan变量赋值给另外一个,则这两个变量访问的是相同的channel。同样,这也意味着可以用make分配一个channel:

    var c = make(chan int)

通信操作符:<-

箭头指示数据流向。

作为一个二元操作符,<-将值从右侧发送到左侧的channel中:

c := make(chan int)
c <- 1 // 向c发送1

作为前缀一元操作符,<- 从一个channel中接收数据:

v = <-c // 从c中接收数据,赋值给v
<-c // 接收数据,丢弃
i := <-c // 接收值,用于初始化i

语义

默认情况下,通信是同步的。(我们后续将讨论异步通信)。这意味着:

1) A在一个channel上的发送操作会阻塞,直到该channel上有一个接收者就绪。
2) 在一个channel上到的接收操作会阻塞,直到该channel上有一个发送者就绪。

因此通信是同步的一种形式:两个通过channel交换数据的goroutine在通信的时刻同步。

让我们泵一些数据吧

func pump(ch chan int) {
    for i := 0; ; i++ { ch <- i }
}
ch1 := make(chan int)
go pump(ch1) // pump挂起; 我们运行
fmt.Println(<-ch1) // 打印 0

现在我们启动一个循环接收者:

func suck(ch chan int) {
    for { fmt.Println(<-ch) }
}
go suck(ch1) // 大量数字出现

你仍可以溜进去,抓取一个值:

fmt.Println(<-ch1) // 输出:3141159

返回channel的函数

在前面的例子中,pump像一个生成器,喷涌出值。但在分配channel等方面做了很多工作。让我们将其打包到一个返回channel的函数中:

func pump() chan int {
    ch := make(chan int)
    go func() {
        for i := 0; ; i++ { ch <- i }
    }()
    return ch
}
stream := pump()
fmt.Println(<-stream)// 打印 0

"返回channel的函数"是Go中的一个重要的惯用法。

到处都是返回channel的函数

我这里不再重复那些你可以从其他地方找到的知名例子。这里有些可以了解一下:

1) prime sieve: 在语言规范以及教程中。

2) Doug McIlroy的Power系列论文:http://plan9.bell-labs.com/who/rsc/thread/squint.pdf

这个程序的一个Go版本在测试套件中:http://golang.org/test/chan/powser1.go

Range和Channel

for循环的range子句接收channel作为一个操作数,在这种情况下,for循环迭代处理从channel接收到的值。我们来重写pump函数;这里是suck的重写,让它也启动一个goroutine:

func suck(ch chan int) {
    go func() {
        for v := range ch { fmt.Println(v) }
    }()
}
suck(pump()) // 现在不再阻塞

关闭一个Channel

range是如何知道何时channel上的数据传输结束了呢?发送者调用一个内置函数close:

    close(ch)

接收者使用"comma ok"测试发送者是否关闭了channel:

    val, ok:= <- ch

当结果为(value, true),说明依然有数据;一旦channel关闭,数据流干,结果将会是(zero, false)。

在一个Channel上使用Range

在一个channel上使用range,诸如:

for value := range <-ch {
    use(value)
}

等价于:

for {
    value, ok := <-ch
    if !ok {
        break
    }
    use(value)
}

Close

关键点:

只有发送者可以调用close。
只有接收者可以询问是否channel被关闭了。
只有在获取值的同时询问(避免竞争)

只有在有必要通知接收者不会再有数据的时候才调用close。

大多数情况下,不需要用close;它与关闭一个文件没有可比性。

不管怎样,channel是可以垃圾回收的。

Channel的方向性

一个channel变量的最简单形式是一个非缓冲(同步的)值,该值可以用于进行发送和接收。

一个channel类型可以被指定为只发或只收:

var recvOnly <-chan int
var sendOnly chan<- int

Channel的方向性(2)

所有Channel创建时都是双向的,但我们可以将它们赋值给带方向性的channel变量。从类型安全性角度考虑,对于函数内的实例非常有用:

func sink(ch <-chan int) {
    for { <-ch }
}
func source(ch chan<- int) {
    for { ch <- 1 }
}
c := make(chan int)//双向的
go source(c)
go sink(c)

同步的Channel

同步的Channel是非缓冲的。发送动作不会完成,直到一个接收者接收这个值。

c := make(chan int)
go func() {
    time.Sleep(60*1e9)
    x := <-c
    fmt.Println("received", x)
}()

fmt.Println("sending", 10)
c <- 10
fmt.Println("sent", 10)

输出:

sending 10 (立即发生)
sent 10 (60秒后,这两行出现)
received 10

异步的Channel

通过告知make缓冲中元素的数量,我们可以创建一个带缓冲的、异步的channel。

c := make(chan int, 50)
go func() {
    time.Sleep(60*1e9)
    x := <-c
    fmt.Println("received", x)
}()
fmt.Println("sending", 10)
c <- 10
fmt.Println("sent", 10)

输出:

sending 10 (立刻发生)
sent 10(现在)
received 10 (60秒后)

缓冲不是类型的一部分

注意缓冲的大小甚至其自身都不是channel类型的一部分,只是值的一部分。因此下面的代码虽危险,但合法:

buf = make(chan int, 1)
unbuf = make(chan int)
buf = unbuf
unbuf = buf

缓冲是一个值的属性,而不是类型的。

Select

select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是send要么是receive。

ci, cs := make(chan int), make(chan string)
select {
    case v := <-ci:
        fmt.Printf("received %d from ci\n", v)
    case v := <-cs:
        fmt.Printf("received %s from cs\n", v)
}

Select随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。

Select语义

快速一览:

- 每个case都必须是一个通信(可能是:=)
- 所有channel表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通信可以进行,它就执行;其他被忽略。
- 如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
- 否则:
    – 如果有default子句,则执行该语句。
    – 如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

随机bit生成器

幼稚但很有说明性的例子:

c := make(chan int)
go func() {
    for {
        fmt.Println(<-c)
    }
}()

for {
    select {
        case c <- 0: //没有语句,没有fallthrough
        case c <- 1:
    }
}

测试可通信性

一个通信是否可以进行,而不阻塞?一个带default字句的select可以告诉我们:

select {
    case v := <-ch:
        fmt.Println("received", v)
    default:
        fmt.Println("ch not ready for receive")
}

如果没有其他case可以运行,那default子句将被执行,因此这对于非阻塞接收是一个惯用法;非阻塞发送显然也可以这么做。

超时

一个通信可以在一个给定的时间内成功完成么?time包包含了after函数:

func After(ns int64) <-chan int64

在指定时间段之后,它向返回的channel中传递一个值(当前时间)。

在select中使用它以实现超时:

select {
case v := <-ch:
    fmt.Println("received", v)
case <-time.After(30*1e9):
    fmt.Println("timed out after 30 seconds")
}

多路复用(multiplexing)

channel是原生值,这意味着他们也能通过channel发送。这个属性使得编写一个服务类多路复用器变得十分容易,因为客户端在提交请求时可一并提供用于回复应答的channel。

chanOfChans := make(chan chan int)

或者更典型的如:

type Reply struct { … }
type Request struct {
    arg1, arg2 someType
    replyc chan *Reply
}

多路复用服务器

type request struct {
    a, b int
    replyc chan int
}

type binOp func(a, b int) int
func run(op binOp, req *request) {
    req.replyc <- op(req.a, req.b)
}

func server(op binOp, service <-chan *request) {
    for {
        req := <-service // 请求到达这里
        go run(op, req) // 不等op
    }
}

启动服务器

使用"返回channel的函数"惯用法来为一个新服务器创建一个channel:

func startServer(op binOp) chan<- *request {
    service := make(chan *request)
    go server(op, req)
    return service
}

adderChan := startServer(
    func(a, b int) int { return a + b }
)

客户端

在教程中有个例子更为详尽,但这里是一个变体:

func (r *request) String() string {
    return fmt.Sprintf("%d+%d=%d",
    r.a, r.b, <-r.replyc)
}
req1 := &request{7, 8, make(chan int)}
req2 := &request{17, 18, make(chan int)}

请求已经就绪,发送它们:

adderChan <- req1
adderChan <- req2

可以以任何顺序获得结果;r.replyc多路分解:

fmt.Println(req2, req1)

停掉

在多路复用的例子中,服务将永远运行下去。要将其干净地停掉,可通过一个channel发送信号。下面这个server具有相同的功能,但多了一个quit channel:

func server(op binOp, service <-chan *request,
            quit <-chan bool) {
    for {
        select {
            case req := <-service:
                go run(op, req) // don't wait for it
            case <-quit:
                return
        }
    }
}

启动服务器

其余代码都相似,只是多了个channel:

func startServer(op binOp) (service chan<- *request,
        quit chan<- bool) {
    service = make(chan *request)
    quit = make(chan bool)
    go server(op, service, quit)
    return service, quit
}

adderChan, quitChan := startServer(
    func(a, b int) int { return a + b }
)

停掉:客户端

只有当准备停掉服务端的时候,客户端才会受到影响:

req1 := &request{7, 8, make(chan int)}
req2 := &request{17, 18, make(chan int)}
adderChan <- req1
adderChan <- req2
fmt.Println(req2, req1)

所有都完成后,向服务器发送信号,让其退出:

quitChan <- true

package main
import ("flag"; "fmt")
var nGoroutine = flag.Int("n", 100000, "how many")
func f(left, right chan int) { left <- 1 + <-right }
func main() {
    flag.Parse()
    leftmost := make(chan int)
    var left, right chan int = nil, leftmost

    for i := 0; i < *nGoroutine; i++ {
        left, right = right, make(chan int)
        go f(left, right)
    }

    right <- 0 // bang!

    x := <-leftmost // 等待完成
    fmt.Println(x)    // 100000
}

例子:Channel作为缓存

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func server() {
    for {
        b := <-serverChan // 等待做work
        process(b) // 在缓存中处理请求
        select {
            case freeList <- b: // 如果有空间,重用缓存
            default:             // 否则,丢弃它
        }
    }
}

func client() {
    for {
        var b *Buffer
        select {
            case b = <-freeList:            // 如果就绪,抓取一个
            default: b = new(Buffer) // 否则,分配一个
        }
        load(b)// 读取下一个请求放入b中
        serverChan <- b // 将请求发给server.
    }
}

并发

并发相关话题

许多并发方面,当然,Go一直在尽力做好它们。诸如Channel发送和接收是原子的。select语句也是缜密定义和实现的等。

但goroutine在共享内存中运行,通信网络可能死锁,多线程调试器糟糕透顶等等。

接下来做什么?

Go给予你原生的

不要用你在使用C或C++或甚至是Java时的方式去编程。

channel给予你同步和通信的能力,并且使得它们很强大,但也可以很容易知道你是否可以很好的使用它们。

规则是:

    不要通过共享内存通信,相反,通过通信共享内存。

特有的通信行为保证了同步!

模型

例如,使用一个channel发送数据到一个专职服务goroutine。如果同一时刻只有一个goroutine拥有指向数据的指针,那谈不上什么并发。

这是我们极力推荐的服务端编程模型,至少是对旧的"每个客户端一个线程"的泛化。它自从20世纪80年代就开始使用了,它工作的很好。

 

内存模型

那关于同步和共享内存的令人生厌的细节在:

http://golang.org/doc/go_mem.html

但如果你遵循我们的方法,你很少需要理解那些内容。

Go程序设计语言(二)

本文译自Rob PikeGo语言PPT教程 – "The Go Programming Language Part 2(updated June 2011)"。由于该教程的最新更新时间早于Go 1版本发布,因此该PPT中的一些内容与Go 1语言规范略有差异,到时我会在相应的地方做上注解。

第二部分大纲

  • 复合类型 – 结构体、数组、切片、Maps
  • 方法 – 不再只是为结构体
  • 接口

数组

数组

Go中的数组与C语言中的数组差异很大,倒更类似Pascal中的数组。 (Slice,下个话题,有些像C语言中的数组)

var ar [3]int

声明ar为一个拥有三个整型数的数组,所有元素初始化为0。

大小是类型的一个组成部分。

内置的函数len可以用于获取数组大小:

len(ar) = 3

数组是值类型

Go中的数组是值,而非C语言中的隐式指针。你可以获得数组的地址,并生成一个指向数组的指针(例如,将其高效地传递给函数):

 

func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }
 
func main() {
    var ar [3] int
    f(ar) // 传递一个ar的拷贝
    fp(&ar) // 传递一个指向ar的指针
}
 
输出结果:

[0 0 0]

&[0 0 0]

数组字面值

所有的符合类型都有相同的值创建语法。以数组为例,其语法如下:
 
3个整数的数组:
  [3]int{1, 2, 3}
 
10个整数的数组,前三个元素不是0:
  [10]int{ 1, 2, 3}
 
不想数?使用…代表长度:
  [...]int{1, 2, 3}
 
不想初始化所有值?使用key:value对:
  [10]int{2:1, 3:1, 5:1, 7:1}
 

指向数组字面值的指针

你可以获取数组字面值的地址,这样可以得到一个指向新建数组实例的指针:
 
func fp(a *[3]int) { fmt.Println(a) }
func main() {
    for i := 0; i < 3; i++ {
        fp(&[3]int{i, i*i, i*i*i})
    }
}
 
输出结果:
 
&[0 0 0]
&[1 1 1]
&[2 4 8]
 

切片(Slice)

切片

切片是对数组中某一段的引用。
 
切片比普通数组应用得更多也更广泛。
 
切片使用的代价很低。
 
一个切片类型很像一个没有大小的数组类型:
  var a []int
 
内置的len(a)可以返回切片中元素的个数。
 
通过对数组或切片进行"切片",我们可以创建一个新切片:
  a = ar[7:9]
 
a(上面例子中的a)的有效下标值是0和1;len(a) == 2
 

切片速记

当对数组进行切片时,第一个下标值默认是0:
   ar[:n]等价于a[0:n]。
 
第二个下标值默认为len(array/slice):
   ar[n:]等价于ar[n:len(ar)]。
 
因此由数组创建切片时:
   ar[:]等价于ar[0:len(ar)]。
 

切片引用数组

概念上:
 
type Slice struct {
    base *elemType // 指向0th元素的指针
    len int // 切片中元素的数量
    cap int // 切片可以容纳元素的数量
}
 
数组:
ar: 7 1 5 4 3 8 7 2 11 5 3
 
切片:
a = ar[7:9] :base = &ar[7](指向ar中的2) len = 2 cap = 4

创建切片

切片字面值看起来像没有指定大小的数组字面值:

 

  var slice = []int{1,2,3,4,5}
 
上面代码创建了一个长度为5的数组并创建一个切片用于引用这个数组。
 
我们可以使用内置的make函数分配一个切片(底层实际是个数组):
  var s100 = make([]int, 100) // slice: 100 ints
 
为何用make而不是用new?因为我们需要创建切片,而不仅仅是为了分配内存。注意make([]int, 10)返回[]int,而new([]int)返回*[]int。
 
使用make创建切片、map以及channel。
 

切片容量

切片是对底层数组的一个引用。因此存在一些在数组里但却没在切片引用的范围内的元素。
 
内置的函数cap(capacity)用于报告切片可能增长到多长。
 
var ar = [10]int{0,1,2,3,4,5,6,7,8,9}
var a = ar[5:7] // 引用子数组{5,6}
 
len(a) = 2,cap(a) = 5,现在我们可以重新切片:
 
a = a[0:4] // 引用子数组 {5,6,7,8}
  

 

len(a)现在是4,而cap(a)依旧是5。
 

调整切片大小

切片可被当作可增长的数组用。使用make分配一个切片,并指定其长度和容量。当要增长时,我们可以做重新切片:
 
var sl = make([]int, 0, 100) // 长度 0, 容量 100
func appendToSlice(i int, sl []int) []int {
    if len(sl) == cap(sl) { error(…) }
    n := len(sl)
    sl = sl[0:n+1] // 长度增加1
    sl[n] = i
    return sl
}
 
因此,sl的长度总是元素的个数,但其容量可根据需要增加。
 
这种手法代价很小,并且是Go语言中的惯用法。
 

切片使用的代价很小

你可以根据需要自由地分配和调整切片大小。它们的传递仅需要很小的代价;不必分配。
 
记住它们是引用,因此下层的存储可以被修改。
 
例如,I/O使用切片,而不是计数:
 
func Read(fd int, b []byte) int
var buffer [100]byte
for i := 0; i < 100; i++ {
    // 每次向Buffer中填充一个字节
    Read(fd, buffer[i:i+1]) // no allocation here
}
 
拆分一个Buffer:
  header, data := buf[:n], buf[n:]
 
字符串也可以被切片,而且效率相似。
 

Maps

maps

Map是另外一种引用类型。它们是这样声明的:
 
var m map[string]float64
 
这里声明了一个map,索引key的类型为string,值类型为float64。这类似于C++中的类型*map<string, float64>。
 
对于给定map m,len(m)返回key的数量。
 

map的创建

和创建一个切片一样,一个map变量是一个空引用;在可以使用它之前,应先要向里面放入一些内容。
 
三种方式:
 
1) 字面值:逗号分隔的key:value对列表
  m = map[string]float64{"1":1, "pi":3.1415}
 
2) 创建
  m = make(map[string]float64) // make not new
 
3) 赋值
  var m1 map[string]float64
  m1 = m // m1和m现在引用相同的map
 

map索引

(接下来的几个例子全都使用:
m = map[string]float64{"1":1, "pi":3.1415})
 
访问一个元素;如果该元素不存在,则得到对应map value类型的零值:
 
one := m["1"]
zero := m["not present"] // zero被置为0.0.
 
设置一个元素的值(两次设置将更新为最新值)
 
m["2"] = 2
m["2"] = 3 // 思维混乱
 

测试存在性

要测试一个map中是否存在某个key,我们可以使用一个多项赋值的"comma, om"形式:
 
m = map[string]float64{"1":1, "pi":3.1415}
 
var value float64
var present bool
 
value, present = m[x]
 
或者按惯例:

value, ok := m[x] // "comma ok" 形式

 
如果map中存在x这个key,布尔变量会被设置为true;value会被赋值为map中key对应的值。相反,布尔变量会被设置为false,value被设置为相应值类型的零值。

 

删除

使用多元赋值可以删除map中的一个值:
 
m = map[string]float64{"1":1.0, "pi":3.1415}
 
var keep bool
var value float64
var x string = f()
 
m[x] = v, keep
 

如果keep的值为true,则将v赋值到map中;如果keep为false,则删除map中的key x。因此删除一个key:

m[x] = 0, false // 从map中删除x

译注:Go 1中上述的删除方式已被取消,取而代之的是delete(m, x)

 

for和range

对于数组、切片和map(以及我们在第三部分将要看到的更多类型),for循环提供了一种特殊的语法用于迭代访问其中的元素。

m := map[string]float64{"1":1.0, "pi":3.1415}

for key, value := range m {
    fmt.Printf("key %s, value %g\n", key, value)
}
 
只用一个变量,我们可以获得key:
 
for key = range m {
    fmt.Printf("key %s\n", key)
}
 
变量可以用:=赋值或声明。
 
对于数组和切片来说,通过这种方式我们可以获得元素的下标以及元素值。
 

将range用于字符串

将for range用于字符串时,实际迭代的元素是Unicode码点(code point),而不是字节(对字节,可使用[]byte或使用标准的for语句)。我们假设字符串包含使用UTF-8编码的字符。
 
下面循环:
s := "[\u00ff\u754c]"
for i, c := range s {
    fmt.Printf("%d:%q ", i, c) // %q for 'quoted'
}
 
输出:0:'[' 1:'ÿ' 3:'界' 6:']'
 
如果遇到了错误的UTF-8码点,这个字符将被设置为U+FFFD,下标向后移动一个字节。

 

 

Structs

structs

对于Go中的struct,你应该感觉十分熟悉:简单的数据字段声明。
 
var p struct {
    x, y float64
}
 
更常用的是:
 
type Point struct {
    x, y float64
}
var p Point
 
struct允许程序员定义内存布局。
 

struct是值类型

struct是值类型,new(StructType)返回一个指向零值的指针(分配的内存都被置0)。
 
type Point struct {
    x, y float64
}
var p Point
p.x = 7
p.y = 23.4
var pp *Point = new(Point)
*pp = p
pp.x = Pi // (*pp).x的语法糖
 
对于结构体指针,没有->符号可用。Go提供了间接的方式。
 

创建结构体

结构体是值类型,因此你可只通过声明就可以创建一个全0的结构体变量。
 
你也可以使用new创建一个结构体。
 
var p Point // 零值
pp := new(Point) // 惯用法
 
结构体字面值语法也不出所料:
 
p = Point{7.2, 8.4}
p = Point{y:8.4, x:7.2}
pp = &Point{7.2, 8.4} // 惯用法
pp = &Point{} //也是惯用法,== new(Point)
 
和数组一样,得到了结构体字面值的地址,就得到了新建结构体的地址。
 
这些例子都是构造器。
 

导出类型和字段

只有当结构体的字段(和方法,即将讲解)名字的首字母大写时,它才能被包外可见。
 
私有类型和字段:
  type point struct { x, y float64 }
 
导出类型和字段:
  type Point struct { X, Y float64 }
 
导出类型和私有类型混合字段:
  type Point struct {
      X, Y float64 // exported
      name string // not exported
  }
 
你甚至可以创建一个带有导出字段的私有类型。(练习:何时能派上用场呢?)
 

匿名字段

在一个结构体内,你可以声明不带名字的字段,比如另外一个结构体类型。这些字段被称为匿名字段。它们看起来就像里层的结构体简单插入或“嵌入”到外层结构体似的。
 
这个简单的机制为从其他类型继承已有的实现提供了一种方法。
 
下面是一个例子。
 

一个匿名结构体字段

type A struct {
    ax, ay int
}
 
type B struct {
    A
    bx, by float64
}
 
B看起来像有四个字段ax、ay、bx和by。B可看成{ax, ay int; bx, by float64}。
 
然后B的字面值必须提供细节:
 
b := B{A{1, 2}, 3.0, 4.0}
fmt.Println(b.ax, b.ay, b.bx, b.by)
 
输出1 2 3 4 
 

匿名字段以类型作为名字

匿名字段不仅仅是简单插入这些字段这么简单,其含义更为丰富:B还拥有字段A。匿名字段看起来就像名字为其类型名的字段。
 
b := B{A{ 1, 2}, 3.0, 4.0}
fmt.Println(b.A)
 
输出:{1 2}。如果A来自于另外一个包,这个字段依旧被称为A。
 
import "pkg"
type C struct { pkg.A }
c := C {pkg.A{1, 2}}
fmt.Println(c.A) // 不是 c.pkg.A
 

任意类型的匿名字段

任何具名类型或指向具名类型的指针都可以用作匿名字段。它们可以出现在结构体中的任意位置。

 

 

type C struct {
    x float64
    int
    string
}
c := C{3.5, 7, "hello"}
fmt.Println(c.x, c.int, c.string)
 
输出:3.5 7 hello
 

冲突和遮蔽

如果有两个字段具有相同的名字(可能是一个继承类型的名字),代码将遵循下面规则:
 
1) 外层的名字遮蔽内层的名字。这提供了一个重写字段/方法的方式。
2) 如果在同一层次上出现了相同的名字,如果名字被使用,那么将是一个错误。(如果没有使用,不会出现错误)
 
二义性是没有规则能解决的,必须被修正。
 

冲突的例子

type A struct { a int }
type B struct { a, b int }
type C struct { A; B }
var c C
 
使用c.a将会出现错误。它到底是c.A.a还是c.B.a呢?
 
type D struct { B; b float64 }
var d D
 
使用d.b没有问题:它是float64类型变量,不是d.B.b。要获得内层的b,可用d.B.b。
 

方法(method)

基于结构体的方法

Go没有类(class),不过你可以为任何类型附上方法。没错,(几乎是)任何类型。方法声明为一个带有显式接收者的函数,其声明独立于类型的声明。下面是一个明显的例子:
 
type Point struct { x, y float64 } // A method on *Point 
 
func (p *Point) Abs() float64 {
    return math.Sqrt(p.x*p.x + p.y*p.y)
}
 
注意:在这个例子中,类型为*Point的显式接收者(不是自动的this)在函数中被使用了。
 

基于结构体值的方法

一个不需要指针作为接收者的方法:
 
type Point3 struct { x, y, z float64 } 
 
// A method on Point3
func (p Point3) Abs() float64 {
    return math.Sqrt(p.x*p.x + p.y*p.y + p.z*p.z)
}
 
这样的代价有些大,因为Point3类型将以值的方式传入到方法中,但在Go中这是合法的。
 

调用一个方法

和你所期望的一样。

 

p := &Point{ 3, 4 }
fmt.Print(p.Abs()) // will print 5
 
一个非结构体的例子:
 
type IntVector []int
func (v IntVector) Sum() (s int) {
    for _, x := range v { // blank identifier!
        s += x
    }
    return
}
fmt.Println(IntVector{1, 2, 3}.Sum())
 

方法的基本规则

方法附属于一个具名类型,比如Foo,并且是静态绑定的。
 
方法中接收者的类型可以是*Foo也可以是Foo。你可以有一些Foo方法,也可以有一些*Foo方法。
 
Foo本身不能是一个指针类型,即便方法可以使用*Foo类型的接收者。
 
类型Foo必须与其全部方法定义在一个包中。
 

指针与值

当你调用方法时,Go自动为你解引用。
 
例如,即使方法的接收者类型为*Point, 你也可以通过可寻址的Point值变量调用该方法。
 
p1 := Point{ 3, 4 }
fmt.Print(p1.Abs()) //  (&p1).Abs()的语法糖
 

同样,如果方法接收者是Point3类型,你 也可以使用一个*Point3类型的指针调用它。

 

p3 := &Point3{ 3, 4, 5 }
fmt.Print(p3.Abs())  // (*p3).Abs()语法糖
 

有关匿名字段的方法

相应的,当一个匿名字段嵌入到一个结构体 中时,这个字段的类型的方法也随之嵌入。– 实际上,它继承了这些方法。
 
这个机制提供了一个模拟子类和继承效果的 简单方式。
 

匿名字段例子

type Point struct { x, y float64 }
func (p *Point) Abs() float64 { … }
type NamedPoint struct {
    Point
    name string
}
n := &NamedPoint{Point{3, 4}, "Pythagoras"}
fmt.Println(n.Abs()) // prints 5
 

重写一个方法

重写工作方式正如字段一样。

 

type NamedPoint struct {
    Point
    name string
}

func (n *NamedPoint) Abs() float64 {
   return n.Point.Abs() * 100.
}

n := &NamedPoint{Point{3, 4}, "Pythagoras"}
fmt.Println(n.Abs()) // prints 500

当然,你可以有多个不同类型的匿名字段 – 一个简单版本的多继承。但冲突解决规则让事情保持简单。

另外一个例子

一个更具吸引力的使用匿名字段的例子。

type Mutex struct { … }
func (m *Mutex) Lock() { … }

type Buffer struct {
    data [100]byte
    Mutex // 在Buffer中不需为第一个字段
}
var buf = new(Buffer)
buf.Lock() // == buf.Mutex.Lock()

注意:Lock的接收者是Mutex字段的地址。而不是外围的结构体。(对比子类或Lisp的mix-ins)

其他类型

方法不仅适用于结构体。他们可以被定义为用于任何非指针类型。

但这个类型必须在你的包中定义。你不能为int编写方法,但你可以声明一个新的int类型,并为其添加方法。

type Day int
var dayName = []string {
    "Monday", "Tuesday", "Wednesday", …
}
func (day Day) String() string {
    return dayName[day]
}

其他类型

现在我们有一个类似枚举的类型,它知道如何打印自己。

const (
    Monday Day = iota
    Tuesday
    Wednesday
    // …
)
var day = Tuesday
fmt.Printf("%q", day.String()) // 打印 "Tuesday"

Print认识string方法

技术上后续会交待,fmt.Print和相近函数可以识别出实现了String方法的值,就像前面定义的类型Day。通过调用这个方法,这些值可以被自动格式化。

于是:
   
fmt.Println(0, Monday, 1, Tuesday)

输出0 Monday 1 Tuesday。

Println可以区分出普通0和值为0的Day类型值。

因此,为你的类型定义一个String方法,这样后续无需再进行其他工作,你的类型就可以获得优雅的输出格式。

方法和字段的可见性

回顾:
  在可见性方面,Go与C++有着很大不同。

1) Go是包作用域,而C++则是文件作用域。
2) 拼写方式决定了是导出的/本地的(公有的/私有的)。
3) 同一包中的结构体有权访问另一个结构体的字段和方法。
4) 本地类型可以导出其字段和方法。
5) 没有真正意义上的子类,没有"protected"符号。

这些规则看起来在实际当中工作良好。

接口

离近点儿观察

我们接下来了解一下Go语言最不同寻常的一点:接口。

请先将你的成见留在门外。

简介

到目前为止,所有我们检视的类型都是具体的:它们实现了一些东西。

还有一个类型需要考虑:接口类型。它是完全抽象的;它不包含任何实现;它提供了一些一个实现必须实现的属性。

接口在概念上非常接近Java,Java中有一个interface类型,但Go的“接口值”概念是非常新颖的。

一个接口的定义

在Go中单词interface似乎有些使用过度了:涉及接口的有接口概念、接口类型以及接口值。

定义:
    一个接口是一组方法的集合。

由一个具体类型,如一个结构体实现的方法形成了那个类型的接口。

例子

之前我们见过这个简单的例子:

type Point struct { x, y float64 }
func (p *Point) Abs() float64 { … }

类型Point的接口拥有方法:

Abs() float64

注意其方法不是:

func (p *Point) Abs() float64

因为接口不应带有接收者的限定。

我们将Point嵌入一个新类型中:NamePoint。NamePoint将具有相同的接口。

接口类型

一个接口类型是一个接口的规格,一组由其他类型来实现的方法。这里是一个简单的例子,只包含一个方法:

type AbsInterface interface {
    Abs() float64 // 接收者是隐式的
}

这是由Point实现的接口的定义,或者用我们的术语来讲,Point实现了AbsInterface。

也可以说成,NamedPoint和Point3实现了AbsInterface方法。

方法写在接口声明内部。

一个例子

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 { return float64(-f) }
    return f
}

MyFloat实现了AbsInterface接口,即便float64没有实现。

顺便:MyFloat不是float64的"装箱"类型;它的表示与float64相同。

多对多

一个接口可以被任意个类型所实现。ABsInterface可以被任何拥有签名如Abs() float64的类型实现,不管该类型是否有其他方法。

一个类型可以实现任意个接口。Point至少实现了下面两个:

type AbsInterface interface { Abs() float64 }
type EmptyInterface interface { }

并且,也许更多,取决于它的方法。

每个类型都实现了EmptyInterface。这将会非常有用。

接口值

一旦一个变量被声明为接口类型,它就可以被赋予任何实现了该接口的类型的值。

var ai AbsInterface
pp := new(Point)
ai = pp // OK:*Point中有Abs方法
ai = 7 // 编译错误, float64没有Abs方法
ai = MyFloat(-7.) // OK:MyFloat有Abs方法
              
ai = &Point{ 3, 4 }
fmt.Printf(ai.Abs())
// 方法调用

输出:5

注意:ai不是指针,它是个接口值。

在内存中

ai不是一个指针!它是一个多字(multiword)数据结构。

ai: receiver value | method table ptr

不同时刻,它的值和类型不同:

ai = &Point{3,4} (*Point在地址0xff1234)

0xff1234| ———–> (*Point) Abs() float64

ai = MyFloat(-7.):

-7. | ——–> (MyFloat) Abs() float64

三个重要事实

1) 接口定义了一组方法。他们是纯洁的且抽象的:没有实现,没有数据字段。Go在接口和实现之间具有清晰的区分。
2) 接口值只是值。它们包含任何实现了接口所有方法的具体值。那些具体值可以是也可以不是指针。
3) 类型通过实现方法来实现接口。它们无需声明它们要做这些事情。例如,每个类型都实现了空接口interface{}。

例子:io.Writer

下面是fmt.Fprintf的实际签名:

func Fprintf(w io.Writer, f string, a … interface{}) (n int, error os.Error)

它不是写入一个文件,而是写入类型为io.Writer的东西中。Writer定义在io包中:

type Writer interface {
    Write(p []byte) (n int, err os.Error)
}

Fprintf因此可以用于写入任何具有Write方法的类型,包括文件、管道、网络链接等。

缓冲I/O

...一个写缓冲。下面来自于bufio包:

type Writer struct { … }

bufio.Writer实现了经典的Write方法。

func (b *Writer) Write(p []byte) (n int, err os.Error)

它还拥有一个工厂方法:传入一个io.Writer,它将以bufio.Writer的形式返回一个缓冲io.Writer:

func NewWriter(wr io.Writer) (b *Writer, err os.Error)

当然,os.File也实现了Writer。

放在一起

import (
    "bufio"; "fmt"; "os"
)
func main() {
    // 无缓冲
    fmt.Fprintf(os.Stdout, "%s, ", "hello")
    // 带缓冲: os.Stdout实现了io.Writer
    buf := bufio.NewWriter(os.Stdout)
    // 现在buf也带缓冲
    fmt.Fprintf(buf, "%s\n", "world!")
    buf.Flush()
}

缓冲可以适合任何Writes的对象。

是不是感觉特像Unix管道啊?可组合性非常强大;参见crypto包。

io包中的其他公共接口

io包拥有:

Reader
Writer
ReadWriter
ReadWriteCloser

这些都是程式化的接口,不过很显然它们捕捉到了任何实现了其名字含义的函数的功能。

这就是为何我们拥有一个带缓冲的I/O包的原因,其实现与I/O自身的实现分开:它同时接受以及提供接口值。

比较

从C++角度去看,接口类型像一个纯抽象类,指定方法,但不实现。

从Java角度去看,接口类型更像是一个Java接口。

然而,在Go中,有一个最大的不同:一个类型不需要声明它要实现的接口,也不需要继承那些接口。如果它实现了相同的方法,它就实现了接口。

其他差异会变得显而易见了。

匿名字段也适用

type LockedBufferedWriter struct {
    Mutex // has Lock and Unlock methods
    bufio.Writer // has Write method
}
func (l *LockedBufferedWriter) Write(p []byte)
(nn int, err os.Error) {
    l.Lock()
    defer l.Unlock()
    return l.Writer.Write(p) // inner Write()
}

LockedBufferedWriter实现了io.Writer,但是通过匿名Mutex类型实现的。

type Locker interface { Lock(); Unlock() }

例子:HTTP服务

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

这是一个在HTTP server包中定义的接口。要提供http服务,可定义一个类型,实现这个接口,连接到服务器(细节省略了)。

type Counter struct {
    n int // or could just say type Counter int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(w, "counter = %d\n", ctr.n)

    ctr.n++
}

现在我们定义一个类型来实现ServeHTTP:

type HandlerFunc func(http.ResponseWriter, *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter,
req *http.Request) {

    f(w, req) // 接收者是一个函数,调用它
}

将函数转换为从属的方法,实现该接口:

var Handle404 = HandlerFunc(notFound)

容器(container)& 空接口

vector实现的梗概。(实际中,倾向于用原始slice替换,但这是有益处的)

type Element interface {}

// Vector本身就是容器.
type Vector []Element

// At()返回第i个元素.
func (p *Vector) At(i int) Element {
    return p[i]
}

Vector可以存储任何类型的元素,因为任何类型都实现了空接口。(事实上,每个元素也可以是不同类型)

类型断言

一旦你像一个Vector中存入一些数据,这个数据将被当成一个接口值存储起来。需要用“拆箱”的方法将其还原:使用“类型断言”,其语法:

interfaceValue.(typeToExtract)

当类型错误时,断言将失败- 不过看下一slide。

var v vector.Vector
v.Set(0, 1234.) // 作为接口值存储
i := v.At(0) // 作为interface{}被获取
if i != 1234. {} // 编译期错误
if i.(float64) != 1234. {} // OK
if i.(int) != 1234 {} // 运行期错误
if i.(MyFloat) != 1234. {} // err: 非MyFloat

类型断言总是在运行期执行。编译器拒绝注定要失败的断言。

接口到接口的转换

到目前为止,我们只将常规值与接口值做了相互转换,但接口值还包含相应的方法,这些方法也可以被转换。

实际上,这与将一个接口值做"拆箱"析出其中的具体值,接着为新接口类型装箱类似。

转换成功与否取决于底层的值,而不是原先的接口类型。

 

接口转换例子

已知:

var ai AbsInterface
type SqrInterface interface { Sqr() float64 }
var si SqrInterface
pp := new(Point) // *Point具有方法Abs, Sqr
var empty interface{}

下面这些都OK:

empty = pp // 所有类型值都满足empty
ai = empty.(AbsInterface) // 底层值实现Abs接口,否则运行时错误
                         
si = ai.(SqrInterface) // *Point实现Sqr(),即使AbsInterface没有

empty = si // *Point 实现了空集
           // 注意: 静态可检查,因此类型断言不是必要的
         

用类型断言测试

可以使用"comma ok"类型断言测试一个值是否是某种类型:

elem := vector.At(0)
if i, ok := elem.(int); ok {
    fmt.Printf("int: %d\n", i)
} else if f, ok := elem.(float64); ok {
    fmt.Printf("float64: %g\n", f)
} else {
    fmt.Print("unknown type\n")
}

用类型switch测试

特殊语法:

switch v := elem.(type) { // 字面值关键字 "type"
case int:
    fmt.Printf("is int: %d\n", v)
case float64:
    fmt.Printf("is float64: %g\n", v)
default:
    fmt.Print("unknown type\n")
}

v实现m()了吗?   

再深入一步,可以测试一个值是否实现了某个方法。

type Stringer interface { String() string }
if sv, ok := v.(Stringer); ok {
    fmt.Printf("implements String(): %s\n",
    sv.String()) // 注意: sv 不是 v
}

这个就是Print等检查某个类型是否可以打印自己的方法。

反射和…

Go提供了一个反射(reflect)包,以支持你通过值探索其类型相关信息。太错综复杂,在这里说不方便。不过我们用Printf来分析一下其参数。

func Printf(format string, args …interface{})(n int, err os.Error)

在Printf内部,args变量变成一个特定类型的slice,例如[]interface{}。并且Printf使用反射包去解包每个元素以分析其类型。

下一个小节有更多有关可变个数参数的函数的内容。

反射和Print

因此,Printf和同族函数知道参数的确切类型。正是因为它们知道参数到底是无符号的或是长整型的,才不需要%u或%ld,只需要%d。

这也是Println和Print可以在没有格式化字符串参数时也可以优雅打印参数的原因。

Printf还有一个%v("值")可以默认打印任何类型的值。

fmt.Printf("%v %v %v %v", -1, "hello",
[]int{1,2,3}, uint64(456))

输出:-1 hello [1 2 3] 456。

事实上,%v等价于由Print和Println完成格式化工作。

可变参数函数

可变参数函数:…

变长参数列表用语法…T声明,T是独立参数的类型。这样的参数必须放在参数列表的末尾。在函数中,变参隐式类型为[]T。

func Min(args …int) int {
    min := int(^uint(0)>>1) // 可能的最大整型值
    for _, x := range args { // args的类型为 []int
        if min > x { min = x }
    }
    return min
}

fmt.Println(Min(1,2,3), Min(-27), Min(), Min(7,8,2))

输出:1 -27 2147483647 2

将slice转换为可变参数

参数变成了一个slice。如果你要将slice直接传递给函数作为参数该如何做呢? 在调用时使用…(只适用于可变参数)

回顾:
    func Min(args …int) int

下面两个调用都返回-2:

Min(1, -2, 3)
slice := []int{1, -2, 3}
Min(slice…) // … 将slice转换为参数

然而,下面的代码将会引发一个类型错误:

Min(slice)

因为slice类型为[]int,而Min的参数必须是独立的int。…是必须的。

Printf用于错误输出

我们可以使用…手法包装Printf或其某个变体来创建我们自己的错误处理函数。

func Errorf(fmt string, args …interface{}) {
    fmt.Fprintf(os.Stderr, "MyPkg: "+fmt+"\n", args…)
    os.Exit(1)
}

我们可以这样使用它:

if err := os.Chmod(file, 0644); err != nil {
    Errorf("couldn't chmod %q: %s", file, err)
}

输出(包括换行符):

MyPkg: couldn't chmod "foo.bar": permission denied

附加(append)

用于加长slice的内置函数append是支持可变参数的。它的函数签名:
    append(s []T, x …T) []T

其中s是个Slice,T是其中元素的类型。它返回一个新slice,即附加了新增元素x的s。

slice := []int{1, 2, 3}
slice = append(slice, 4, 5, 6)
fmt.Println(slice)

打印: [1 2 3 4 5 6]

只要可能,append就会在正确的位置上增加slice。

附加一个slice

如果你想附加一个整个slice,而不是单个元素,我们再一次在调用时使用…。

slice := []int{1, 2, 3}
slice2 := []int{4, 5, 6}
slice = append(slice, slice2…) // …是必须的
fmt.Println(slice)

这里例子也打印[1 2 3 4 5 6]。

Go程序设计语言(一)

本文译自Rob PikeGo语言PPT教程 – "The Go Programming Language Part 1(updated June 2011)"。由于该教程的最新更新时间早于Go 1版本发布,因此该PPT中的一些内容与Go 1语言规范略有差异,到时我会在相应的地方做上注解。

谁发明了Go

Go语言的设计和实现工作是由Google的一个研发小组以及来自世界各地的大量贡献者共同完成的。

联系方式:

课程大纲

第一部分:基础
第二部分:类型(type)、方法(method)以及接口(interface)
第三部分:并发(concurrency)与通信(communication)

这个课程是关于Go程序设计语言的,而不是关于编程语言设计方法的,后者是一个单独的话题,不在此教程范围内。

第一部分大纲

  • 动机
  • 基础 - 简单、熟悉的内容
  • 包与程序构建

动机

为什么要发明一门新语言?

在当今世界,编程语言在某些方面不够给力:

  • 计算机运行速度快,但软件的构建慢。
  • 为了速度和安全性需进行必要的依赖分析。
  • 在类型上遇到了太多的阻碍。
  • 对垃圾收集以及并发的支持太差。
  • 多核被视为危机而不是机会。

积极应对

我们的目标是让编程重新回归快乐。

  • 兼有动态语言的感觉以及静态类型系统的安全性;
  • 编译成机器语言以获得更快的运行速度;
  • 真正在运行时支持GC(垃圾收集)以及并发;
  • 轻量级、灵活的类型系统;
  • 拥有方法(method),但却不是传统的OO(面向对象)语言。

资源

关于Go语言的更多背景资料请参见文档:http://golang.org

文档包括:

  • 语言规范
  • 教程
  • "Effective Go"
  • 标准库文档
  • 安装和How-to文档
  • FAQs
  • 一个语言联系游乐场(在浏览器中运行Go程序)
  • 更多

现状:编译器

gc(Ken Thompson),又称6g,8g,5g
        继承自Plan 9项目的编译器模型
        生成代码速度非常快
        不支持gcc直接链接

gccgo(Ian Taylor)
        更为熟悉的体系架构
        生成代码的速度没有gc那样快
        支持gcc直接链接

支持32-bit和64-bit x86 (amd64,x86-64) 以及ARM。

垃圾收集器,并发等都已实现。
优秀且正逐步完善的标准库。

基础

是时候上一些代码了

package main

import "fmt"
 
func main() {
    fmt.Print("Hello, 世界\n")
}
 

语言基础

  • 假设熟悉其他C语言类的(C-like)编程语言,这里将快速浏览一些基础知识。
  • 这里大部分内容是简单的且熟悉的,也可能因此而有些沉闷,这里先说声道歉。
  • 接下来的两部分教程会包含很有趣的内容,不过我们首先需要打下良好基础。

词法结构

- 传统中蕴含新意。
- 源码采用UTF-8编码。空格包括:空白,tab,换行,回车。
- 标识符由字母和数字组成(外加'_'),字母和数字都是Unicode编码。
- 注释:
/* This is a comment; no nesting */
// So is this.
 

字面值(literals)

类似C语言中的字面值,但数值不需要符号以及大小标志(后续会有更多这方面内容):
 
23
0x0FF
1.234e7
 
类似C中的字符串,但字符串是Unicode/UTF-8编码的。同时,\xNN总是有2个数字;\012总是3;两个都是字节:
 
"Hello, world\n"
"\xFF"       // 1 byte
"\u00FF"     // 1 Unicode char, 2 bytes of UTF-8
 
原生字符串:`\n\.abc\t\` == "\\n\\.abc\\t\\"

语法概述

基本上就是类C的语法,但使用反转的类型和声明,并使用关键字作为每个声明的开头。
 
var a int
var b, c *int // 注意与C的不同
var d []int
type S struct { a, b int }
 
基本的控制结构也十分熟悉:
 
if a == b { return true } else { return false }
for i = 0; i < 10; i++ { … }
 
注意:没有圆括号,但需要大括号。
 
后续会有更多有关这方面的内容。
 

分号

分号作为语句终止符号,但:
- 如果前一个符号是语句的结尾,那词法分析程序将自动在行尾插入一个分号
- 注意:比JavaScript的规则更清晰和简单

因此,下面的程序不需要分号:

package main

const three = 3
var i int = three

func main() { fmt.Printf("%d\n", i) }

在实际中,Go源码在for和if子句之外几乎都没有用到分号。

数值类型

数值类型(numeric types)是原生内置的,也是为大家所熟知的:

int          uint
int8      uint8 = byte
int16       uint16
int32       uint32         float32      complex64
int64       uint64         float64      complex128

还有uintptr,一个大小足够存储一个指针的数值。

这些都是互不相同的类型;int不等于是int32,即便是在一个32位的机器上。

没有隐式类型转换(不过不要恐慌)。

Bool

普通的布尔类型bool,取值true和false(预定义的常量)。

if语句等使用布尔表达式。

指针类型和整型不是布尔类型。

string

原生内置的string类型代表不可改变的字节数组,即文本。string类型是用长度定界的,而不是以结尾0终止的。

字符串字面值是string类型。

和整型一样不可改变。可重新赋值,但不能修改其值。

正如"3"总是3,"hello"也总是"hello"。

Go语言对字符串操作提供了良好的支持。

表达式(Expressions)

大多都是类C语言的操作符。

二元操作符:

优先级                操作符                    备注

5                 * / % << >> & &^         &^是位清理操作符
4                 + – | ^                  ^是异或(xor)

3                 == != < <= > >=
2                 &&
1                 ||

一元操作符包括:& ! * + – ^(外加用于通信的<-)
一元操作符^是求补码/反码操作。

Go vs. C表达式

可以让C程序员惊喜的是:

更少的优先级层次(应该容易)。
^替代了~
++和–不再是表达式操作符(x++是一个语句,不是表达式;*p++是(*p)++,而不是*(p++))
&^是新操作符,在常量表达式中很有用
<<和>>等需要一个无符号的移位计数。

无惊喜的是:

赋值操作与所期望的一样:+= <<= &^=等
表达式总体看起来相似(下标、函数调用等)

例子

+x
23 + 3*x[i]
x <= f()
^a >> b
f() || g()
x == y + 1 && <-ch > 0
x &^ 7 // x with the low 3 bits cleared
fmt.Printf("%5.2g\n", 2*math.Sin(PI/8))
7.234/x + 2.3i

"hello, " + "world"  // concatenation
                     // no C-like "a" "b"

数值转型

将一个数值从一个类型转换为另一个类型称为一次转型,其语法形式有点类似函数调用:

uint8(intVar)   //截断到相应的大小
int(float64Var) //片段截断
float64(intVar) //转为float64

一些涉及string类型的转型:

string(0×1234)          // == "\u1234"
string(sliceOfBytes)    // bytes -> bytes
string(sliceOfInts)     // ints -> Unicode/UTF-8
[]byte("abc")           // bytes -> bytes
[]int("日本語")          // Unicode/UTF-8 -> ints

切片(slice)与数组相关,稍后会有更多相关内容。

常量

数值常量是"理想数":没有大小或标志,因此没有U、L或UL作结尾。

077 // 八进制
0xFEEDBEEEEEEEEEEEEEEEEEEEEF //十六进制
1 << 100

下面是整数和浮点数值,字面值的语法决定其类型:

1.234e5    // 浮点
1e2        // 浮点
3.2i       // 浮点虚数
100        // 整数

常量表达式

浮点和整型常量可以任意组合,最终表达式的类型由常量的类型决定。操作自身也取决于类型。

2*3.14   // 浮点: 6.28
3./2     // 浮点:1.5
3/2      // 整型:1
3+2i     // 复数:3.0 + 2.0i

// 高精度
const Ln2 = 0.69314718055994530941723212145817656807
const Log2E = 1/Ln2 

数值的表示范围足够大(目前最大用1024位表示)。

理想数的结果

Go语言允许无需显式转型的情况下使用常量,前提是常量值可以被其类型表示(没有必要进行转型;其值表示起来没问题):

var million int = 1e6  //float语法在这里可以使用
math.Sin(1)

常量必须可以被其类新表示。例如:^0的值为-1,不在0-255的范围内。

uint8(^0)       //错误:-1无法用uint8类型表示
^uint8(0)       //OK
uint8(350)      //错误:350无法用uint8类型表示
uint8(35.0)     //OK: 35
uint8(3.5)      //错误:3.5无法用uint8类型表示

声明

声明以一个关键字开头(var, const,type和func),并且与C中的声明次序相反:

var i int
const PI = 22./7.
type Point struct { x, y int }
func sum(a, b int) int { return a + b }

为何要以相反次序声明呢?早期的一个例子:

var p, q *int

p和q的类型都是*int。并且函数读起来更佳,并且与其他声明一致。还有一个原因,马上道来。

Var

变量声明以var开头。

它们可以有一个类型或一个初始化表达式;至少应有一个或二者都有。初始化表达式应该与变量匹配(还有类型!)。

var i int
var j = 365.245
var k int = 0
var l, m uint64 = 1, 2
var nanoseconds int64 = 1e9 // float64 constant!
var inter, floater, stringer = 1, 2.0, "hi"

分派var

总是输入var让人生厌。我们可以通过括号让多个变量声明成为一组:

var (
    i int
    j = 356.245
    k int = 0
    l, m uint64 = 1, 2
    nanoseconds int64 = 1e9
    inter, floater, stringer = 1, 2.0, "hi"
)

这种形式适用于const,type, var,但不能用于func。

=:"短声明"

在函数内(只有在函数内这一种情况下),下面形式的声明:
  var v = value

可以被缩短成:
  v := value

(这就是另外一个名字、类型倒序的原因)

类型就是值的类型(对于理想数,相应的类型是int或float64或complex128)
  a, b, c, d, e := 1, 2.0, "three", FOUR, 5e0i

这种形式的声明使用很频繁,并且在诸如for循环初始化表达式中也可以使用。

Const

常量声明以const开头。

它们必须有一个常量表达式,可在编译期间求值,作为初始化表达式,可以拥有一个可选的类型修饰符。

const Pi = 22./7.
const AccuratePi float64 = 355./113
const beef, two, parsnip = "meat", 2, "veg"
const (
    Monday, Tuesday, Wednesday = 1, 2, 3
    Thursday, Friday, Saturday = 4, 5, 6
)

Iota

常量声明可以使用计数器:iota,每个const块中的iota都从0开始计数,在每个隐式的分号(行尾)自增。

const (
    Monday = iota  // 0
    Tuesday = iota // 1
)

速记:重复上一个类型和表达式。

const (
    loc0, bit0 uint32 = iota, 1<<iota //0,1
    loc1, bit1                        //1,2
    loc2, bit2                        //2,4
)

Type

类型声明以type开头。

我们后续会学习更多类型,不过先这里举几个例子:

type Point struct {
    x, y, z float64
    name
    string
}
type Operator func(a, b int) int
type SliceOfIntPointers []*int

我们稍后会回到函数。

New

内置函数new用于分配内存。其语法类似一个函数调用,以类型作为参数,与C++中的new类似。返回一个指向已分配对象的指针。

var p *Point = new(Point)
v := new(int)   // v的类型为*int

稍后我们将看到如何构建切片(slice)

Go语言中没有用于内存释放的delete或free。Go具备垃圾回收功能。

赋值

赋值是容易和熟悉的:

a = b

但Go还支持多项赋值:

x, y, z = f1(), f2(), f3()
a, b = b, a  //交互a,b的值

函数支持多个返回值(稍后有更多细节):

nbytes, error := Write(buf)

控制结构

与C类似,但很多地方有不同。

Go支持if、for和switch。

正如之前说的,无需小括号,但大括号是必要的。

如果将它们看为一组,它们的用法很规律。例如,if、for和switch都支持初始化语句。

控制结构的形式

后续会有细节,但总体上:

if和switch语句以1元素和2元素形式呈现,后面详细讲解。

for循环具有1元素和3元素的形式:

1元素形式等价于C语言中的while:
    for a {}
3元素形式等价于C语言中的for:
    for a;b;c {}

在所有这些形式里,任何元素都可以是空。

if

基本形式是大家所熟知的,但已经没有了"else悬挂"问题了:

if x < 5 { less() }
if x < 5 { less() } else if x == 5 { equal() }

支持初始化语句;需要分号。

if v := f(); v < 10 {
    fmt.Printf("%d less than 10\n", v)
} else {
    fmt.Printf("%d not less than 10\n", v)
}

与多元函数一起使用更有益处:

if n, err = fd.Write(buf); err != nil { … }

省略条件意为true,在这里没有什么用。但在for,switch语句中尤其有用。

for

基本形式是大家所熟知的:
    for i := 0; i < 10; i++ { … }

省略条件意为true:

    for ;; { fmt.Printf("looping forever") }

而且你还可以省略分号:
   
    for { fmt.Printf("Mine! ") }

不要忘记多项赋值:
    for i,j := 0,N; i < j; i,j = i+1,j-1 {…}

(Go中没有像C中那样的逗号操作符)

switch细节

switch与C中的switch有些类似。

不过,有一些语法和语义的重要不同之处:
- 表达式不必一定是常量,甚至可以不必是int。
- 没有自动的fall through
- 但作为替代,语法上,最后的语句可以为fallthrough
- 多case可以用逗号分隔

switch count%7 {
    case 4,5,6: error()
    case 3: a *= v; fallthrough
    case 2: a *= v; fallthrough
    case 1: a *= v; fallthrough
    case 0: return a*v
}

Switch

Go中的switch要远比C中的强大。常见的形式:

switch a {
    case 0: fmt.Printf("0")
    default: fmt.Printf("non-zero")
}

switch表达式可以是任意类型,如果为空,则表示true。结果类似一个if-else链:

a, b := x[i], y[j]
switch {
    case a < b: return -1
    case a == b: return 0
    case a > b: return 1
}

switch a, b := x[i], y[j]; { … }

Break,continue等

break和continue语句的工作方式与C中的类似。

它们可以指定一个label并影响外层结构:

Loop: for i := 0; i < 10; i++ {
    switch f(i) {
        case 0, 1, 2: break Loop
    }
    g(i)
}

是的,那是一个goto。

函数

函数以func关键字开头。

如果有返回类型,返回类型放在参数的后面。return的含义和你期望的一致。

func square(f float64) float64 { return f*f }

函数支持返回多个值。这样,返回类型就是一个括号包围的列表。

func MySqrt(f float64) (float64, bool) {
    if f >= 0 { return math.Sqrt(f), true }
    return 0, false
}

空标识符

如果你只关心MySqrt函数返回的第一个值?你仍然需要将第二个值放在一个地方。

解决方法:使用空标识符_(下划线)。它是预声明的,可以被赋予任何无用的值。

// Don't care about boolean from MySqrt.
val, _ = MySqrt(foo())

在空标识符其他的适用场合中,我们仍然会展示它。

带结果变量(result variable)的函数

如果你给结果参数命名了,你可以将它当作实际变量使用。

func MySqrt(f float64) (v float64, ok bool) {
    if f >= 0 { v,ok = math.Sqrt(f), true }
    else { v,ok = 0,false }
    return v,ok
}

结果变量被初始化为"0"(0,0.0,false等。根据其类型;稍后有更多有关内容)

func MySqrt(f float64) (v float64, ok bool) {
    if f >= 0 { v,ok = math.Sqrt(f), true }
    return v,ok
}

空返回

最后,一个没有返回表达式的return将返回结果变量的当前值。下面是另外两个MySqrt的版本:

func MySqrt(f float64) (v float64, ok bool) {
    if f >= 0 { v,ok = math.Sqrt(f), true }
    return // must be explicit
}
func MySqrt(f float64) (v float64, ok bool) {
    if f < 0 { return } // error case
    return math.Sqrt(f),true
}

0是什么

Go中的内存都是被初始化了的。所有变量在执行之前的声明时被初始化。如果没有显式的初始化表达式,我们将使用对应类型的"0值"。下面的循环:

for i := 0; i < 5; i++ {
    var v int
    fmt.Printf("%d ", v)
    v = 5
}
将打印0 0 0 0 0。

0值取决于类型:数值是0;布尔是false;空字符串是"";指针,map、切片、channel是nil;结构体是0等。

Defer

defer语句负责在其所在的函数返回时执行一个函数(或方法)。其参数在到达defer语句那个时刻被求值;其函数在返回时被执行。

func data(fileName string) string {
    f := os.Open(fileName)
    defer f.Close()
    contents := io.ReadAll(f)
    return contents
}

在关闭文件描述符、解互斥锁等场合十分有用。

每Defer执行一个函数

Go按按后入先出(LIFO)次序执行一组defer函数。

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

上面代码将输出4 3 2 1 0。你可以在最后关闭所有文件描述符以及解锁所有互斥锁。

用defer跟踪代码

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

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

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

func main() { b() }

不过我们可以实现的更灵巧一些。

参数当即求值,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() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}
func main() { b() }

函数字面值

和在C中一样,函数不能在函数内部声明。但函数字面值却可以被赋值给变量。

func f() {
    for i := 0; i < 10; i++ {
        g := func(i int) { fmt.Printf("%d",i) }
        g(i)
    }
}

函数字面值是闭包(closure)

函数字面值实际上是闭包。

func adder() (func(int) int) {
    var x int
    return func(delta int) int {
        x += delta
        return x
    }
}

f := adder()
fmt.Print(f(1))
fmt.Print(f(20))
fmt.Print(f(300))

输出1 21 321 – f中的x累加。

程序构建

包(package)

一个程序以一个包的形式构建,这个包还可以使用其他包提供的一些设施。

一个Go程序的创建是通过链接一组包。

一个包可以由多个源码文件组成。

导入包中的名字可以通过packagename.Itemname访问。

源码文件结构

每个源码文件包括:

- 一个package字句(文件归属于哪个包);其名字将作为导入包时的默认名字。
    package fmt
- 一个可选的import声明集
    import "fmt"  //使用默认名字
    import myFmt "fmt" //使用名字myFmt

- 0个或多个全局或“包级别”声明。

单一文件包

package main // 这个文件是包main的一部分

import "fmt" // 这个文件使用了包"fmt"

const hello = "Hello, 世界\n"

func main() {
    fmt.Print(hello)
}

main和main.main

每个Go程序包含一个名为main的包以及其main函数,在初始化后,程序从main开始执行。类似C,C++中的main()函数。

main.main函数没有参数,没有返回值。当main.main返回时,程序立即退出并返回成功。

os包

os包提供Exit函数以及访问文件I/O以及命令行参数的函数等。

// A version of echo(1)
package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) < 2 { // length of argument slice
        os.Exit(1)
    }
    for i := 1; i < len(os.Args); i++ {
        fmt.Printf("arg %d: %s\n", i, os.Args[i])
    }
} // falling off end == os.Exit(0)

全局作用域与包作用域

在一个包中,所有全局变量、函数、类型以及常量对这个包的所有代码可见。

对于导入该包的包而言,只有以大写字母开头的名字是可见的:全局变量、函数、类型、常量以及方法和结构体中全局类型以及变量的字段。

const hello = "you smell"    // 包内可见
const Hello = "you smell nice"  //全局可见
const _Bye = "stinko!"        // _不是大写字母

这与C/C++非常不同:没有extern、static、private以及public。

初始化

有两种方法可以在main.main执行前初始化全局变量:
1) 带有初始化语句的全局声明
2) 在init函数内部,每个源文件中都可能有init函数。

包依赖可以保证正确的执行顺序。

初始化总是单线程的。

初始化例子

package transcendental

import "math"

var Pi float64

func init() {
    Pi = 4*math.Atan(1) // init function computes Pi
}
====
package main

import (
    "fmt"
    "transcendental"
)

var twoPi = 2*transcendental.Pi // decl computes twoPi

func main() {
    fmt.Printf("2*Pi = %g\n", twoPi)
}
====
输出: 2*Pi = 6.283185307179586

包与程序构建

要构建一个程序,包以及其中的文件必须按正确的次序进行编译。包依赖关系决定了按何种次序构建包。

在一个包内部,源文件必须一起被编译。包作为一个单元被编译,按惯例,每个目录包含一个包,忽略测试,

cd mypackage
6g *.go

通常,我们使用make; Go语言专用工具即将发布(译注:Go 1中可直接使用go build、go install等高级命令,可不再直接用6g、6l等命令了。)

构建fmt包

% pwd
/Users/r/go/src/pkg/fmt
% ls
Makefile fmt_test.go format.go print.go # …
% make # hand-written but trivial
% ls
Makefile _go_.6 _obj fmt_test.go format.go print.go # …
% make clean; make

目标文件被放在_obj子目录中。

编写Makefiles时通常使用Make.pkg提供的帮助。看源码。

测试

要测试一个包,可在这个包内编写一组Go源文件;给这些文件命名为*_test.go。

在这些文件内,名字以Test[^a-z]开头的全局函数会被测试工具gotest自动执行,这些函数应使用下面函数签名:

func TestXxx(t *testing.T)

testing包提供日志、benchmarking、错误报告等支持。

一个测试例子

摘自fmt_test.go中的一段有趣代码:

import (
    "testing"
)

func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for i := 0; i < len(flagtests); i++ {
        tt := flagtests[i]
        s := Sprintf(tt.in, &flagprinter)
        if s != tt.out {
            // method call coming up – obvious syntax.
            t.Errorf("Sprintf(%q, &flagprinter) => %q,"+" want %q", tt.in, s, tt.out)
        }
    }
}

gotest(译注:在go 1中gotest工具用go test命令替代)

% ls
Makefile fmt.a fmt_test.go format.go print.go # …
% gotest # by default, does all *_test.go
PASS
wally=% gotest -v fmt_test.go
=== RUN fmt.TestFlagParser
— PASS: fmt.TestFlagParser (0.00 seconds)
=== RUN fmt.TestArrayPrinter
— PASS: fmt.TestArrayPrinter (0.00 seconds)
=== RUN fmt.TestFmtInterface
— PASS: fmt.TestFmtInterface (0.00 seconds)
=== RUN fmt.TestStructPrinter
— PASS: fmt.TestStructPrinter (0.00 seconds)
=== RUN fmt.TestSprintf
— PASS: fmt.TestSprintf (0.00 seconds) # plus lots more
PASS
%

一个benchmark的测试例子

Benchmark的函数签名如下:

func BenchmarkXxxx(b *testing.B)

并被循环执行b.N次;其余的由testing包完成。

下面是一个来自fmt_test.go中的benchmark例子:

package fmt // package is fmt, not main
import (
    "testing"
)
func BenchmarkSprintfInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Sprintf("%d", 5)
    }
}

Benchmarking: gotest

% gotest -bench="." # regular expression identifies which
fmt_test.BenchmarkSprintfEmpty 5000000
310 ns/op
fmt_test.BenchmarkSprintfString 2000000
774 ns/op
fmt_test.BenchmarkSprintfInt
5000000
663 ns/op
fmt_test.BenchmarkSprintfIntInt 2000000
969 ns/op

%

库就是包。

目前的库规模是适中的,但还在增长。

一些例子:

包                目的             例子
fmt            格式化I/O           Printf、Scanf
os             OS接口              Open, Read, Write
strconv        numbers<-> strings  Atoi, Atof, Itoa
io             通用I/O             Copy, Pipe
flag           flags: –help等     Bool, String
log            事件日志             Logger, Printf
regexp         正则表达式           Compile, Match
template       html等              Parse, Execute
bytes          字节数组             Compare, Buffer

更多关于fmt

fmt包包含一些熟悉的名字:

Printf – 打印到标准输出
Sprintf – 返回一个字符串
Fprintf – 写到os.Stderr等

还有

Print, Sprint, Fprint – 无格式no format
Println, Sprintln, Fprintln – 无格式,但中间加入空格,结尾加入\n

fmt.Printf("%d %d %g\n", 1, 2, 3.5)
fmt.Print(1, " ", 2, " ", 3.5, "\n")
fmt.Println(1, 2, 3.5)

每个都输出相同的结果:"1 2 3.5\n"

库文档

源码中包含注释。

命令行或web工具可以将注释提取出来。

链接:http://golang.org/pkg/

命令:
    % godoc fmt
    % godoc fmt Printf




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多