Golang作为一门新的编程语言,它借鉴了现有语言的思想但拥有着不同寻常的特性,使得有效的Go程序在性质上不同于其
亲属
编写的程序。如果直接将C++或Java程序翻译成Go程序也不太可能产生令人满意的结果,因为从Go的角度来思考这个问题,可以产生一个成功但又完全不同的程序。要写得好,理解它的性质和习语及Golang中既定约定(如命名、格式化、程序构造等)对于提供编写清晰、惯用go代码的技巧很有帮助,这样编写出的程序也便于其他Golang程序员理解。Golang素以简洁高效著称,我想这是对学习并掌握好语言的基础知识并加以灵活运用的效果。本文将从Golang编程语言的数据类型、语法规则、语言特性方面进行介绍
基本语法
Formatting
格式化问题是最有争议但最不一致的。人们可以适应不同的格式样式,如果每个人都坚持相同的样式,那么可以花费更少的时间在格式化主题上。在go语言中采取了一种不寻常的方法,让机器处理大多数格式化问题。gofmt程序(也可用作go fmt,在包级别而不是源文件级别运行)读取go程序,并以缩进和垂直对齐的标准样式发布源代码,保留并在必要时重新格式化注释
## 示例
package main
import "fmt"
func main() {
a:= 12//initial a
fmt.Printf("hello, world, a = %v\n", a)//print
}
## gofmt格式化: gofmt -s -w hello.go
package main
import "fmt"
func main() {
a := 12 //initial a
fmt.Printf("hello, world, a = %v\n", a) //print
}
标准包中的所有go代码都已用gofmt格式化。一些格式细节仍然保留,简要说明
Indentation 使用制表符缩进,默认情况下gofmt会发布制表符,只有在必要时才使用空格
Line length go没有行长度限制,如果一行太长,用一个额外的标签把它包起来缩进
-
Parentheses go需要的括号比C和Java少: 控制结构(if, for, switch)的语法中没有括号;同样,运算符的优先级层次越短越清晰,因此,
x<<8 + y<<16
注解 Go提供C风格的块注解/**/及C++风格的行注解//。行注释是规范;块注释主要作为包注释出现,但在表达式中很有用,或用于禁用大片代码。godoc程序通过源文件来提取有关包内容的文档,在顶级声明之前出现的注释(没有中间的换行符)将与声明一起提取,作为解释文本。这些注释的性质和样式决定了godoc生成的文档的质量。每个包都应该有一个包注释,即package子句前面的一个块注释。对于多文件包,包注释只需要出现在一个文件中,任何一个都可以。包注释应介绍包并提供相关信息给整个包。它将首先出现在godoc页面上,并应设置以下详细文档
/*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp
如果包很简单,包注释可以很简短
// Package path implements utility routines for
// manipulating slash-separated filename paths.
评论不需要额外的格式,生成的输出甚至可能不会以固定宽度的字体显示,所以不要依赖于对齐的间距,gofmt会处理这个问题。注释是纯文本,因此html和其他注释将逐字复制,不应使用。godoc做的一个调整是以固定宽度的字体显示缩进的文本,适合程序片段
在包中,顶级声明前面的任何注释都将作为该声明的文档注释。程序中的每个导出(首字母大写)名称都应该有一个doc注释。doc注释作为完整的句子最有效,它允许各种各样的自动化演示。第一句话应该是一句话的总结,以所声明的名称开头
// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {
命名 命名在Go语言和其他语言中一样重要,它们甚至具有语义效果: 包外名称的可见性取决于其第一个字符是否大写。因此,在go程序中花点时间讨论命名约定是值得的
包名 当包被导入时,包名称将成为内容的访问器
import "bytes"
导入包后可以调用bytes.Buffer; 如果每个使用包的人都可以使用相同的名称来引用它的内容,这将很有帮助,这意味着包的名称应该具有:简短、简洁、令人印象深刻的特点。按照惯例,包的名称是小写的、单字的;不需要下划线或混合大写;包名称是其源目录的基名称;src/encoding/base64中的包被导入为“encoding/base64”,但名称为base64,而不是base64中的编码,也不是encodingbase64
Getters go不为getter和setter提供自动支持。自己提供getter和setter并没有错,这样做通常是合适的,但不是惯用的,也不是必须使用getter的名称。如果有一个名为owner的字段(小写,不可导出),则getter方法应称为Owner(大写,可导出),而不是GetOwner。在导出中使用大写名称提供一个钩子来区分字段和方法。如果需要,setter函数可能会被称为SetOwner
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
接口名称 按照惯例,接口由方法名加上er后缀或类似的修改来命名,例如: reader、writer、formatter、closenotifier等
MixedCaps go中的约定是使用MixedCaps或mixedCaps格式来编写多个单词的名称,而不是使用下划线连接
分号 与C一样,go的形式语法使用分号来终止语句,但与c不同的是,这些分号不会出现在源代码中,lexer使用一个简单的规则在扫描时自动插入分号,因此输入的文本基本上没有分号。规则:如果换行符之前的最后一个标记是标识符(包括int和float64等词)、基本文本(如数字或字符串常量)或其中一个标记,则lexer始终在该标记之后插入分号。这可以概括为:“如果换行符位于可以结束语句的标记之后,请插入分号”
break continue fallthrough return ++ -- ) }
在右大括号之前也可以省略分号; go程序习惯用法只有在for循环子句等位置才有分号,以分隔初始值设定项、条件和循环元素。它们也是在一行中分隔多个语句所必需的,您应该这样编写代码
go func() { for { dst <- <-src } }()
分号插入规则的一个结果是,不能将控件结构的左大括号(if、for、switch或select)放在下一行。如果这样做,将在大括号之前插入分号,这可能会造成不必要的影响
// like this
if i < f() {
g()
}
// 而不是
if i < f() // wrong!
{ // wrong!
g()
}
控制结构 go的控制结构与C的控制结构有关,但存在诸多不同。没有do或while循环,只有一个轻微泛化的for;switch更灵活;if和switch接受一个类似for的可选初始化语句;break和continue语句使用一个可选标签来标识要中断或继续的语句块;新的控制结构包括类型开关和多路通信复用器--select。语法也略有不同: 没有括号,正文必须始终用大括号分隔
If go中一个简单的if语句类似
if x > 0 {
return y
}
强制大括号鼓励在多行上编写简单的if语句。无论如何,这样做是一种好的风格,尤其是当主体包含这样一个返回或中断的控制语句时。if和switch接受初始化语句,所以通常会看到用于设置局部变量的语句
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
在go库中,您会发现当一个语句没有流入下一个语句(即,主体以break、continue、goto或return结尾)时,不必要的else语句将被省略
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
这是一个常见情况的例子,代码必须防止一系列错误条件。如果成功的控制流在页面上运行,代码读起来很好,从而消除了出现的错误情况。由于错误情况往往以返回语句结尾,因此生成的代码不需要其他语句
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
重新声明并且重新赋值 :=
符号声明并赋值,参考以上一个示例
## 调用os.Open(),声明两个变量f和err
f, err := os.Open(name)
## 接下来调用f.Stat(),声明变量d和err
d, err := f.Stat()
err变量出现在两个语句中,这种重复是合法的: err在第一个语句中声明,在第二个语句中重新分配。这意味着对f.Stat()的调用使用了上面声明的现有err变量,并给它分配一个新的值
在 := 声明中,即使变量v已经声明,也可能再出现,前提是
- 此声明与v的现有声明在同一作用域之内;如果v已经在外部作用域声明,则此声明将创建一个新变量§
- 初始化中的相应值可分配给v,并且
- 声明中至少有一个变量是新的(第一次声明)
For go的for循环类似于但又不同于C,它统一了for和while,没有do-while。以下三种表达形式,只有一种带有分号
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
简短的语句使在循环中声明索引变量变得很容易
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
如果在数组、切片、字符串或map上循环,或从channel读取,则使用range子句可以管理循环
for key, value := range oldMap {
newMap[key] = value
}
如果只需要range中的第一项(key或索引号),可以丢弃第二项
for key := range m {
if key.expired() {
delete(m, key)
}
}
如果只需要range中的第二项(值),使用空白标识符(下划线)放弃第一项
sum := 0
for _, value := range array {
sum += value
}
对于字符串,range通过解析UTF-8来分解各个Unicode编码,错误的编码消耗一个字节并产生替换的rune(宽字符) U+FFFD。rune,Go术语,表示单个Unicode编码(相关的内置类型)
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
## 打印结果如下
character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7
go没有逗号运算符,++和--are语句不是表达式。因此,如果要在for中运行多个变量,则应使用并行赋值
// 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。如果switch没有表达式,switches on true。因此,将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
}
不存在自动筛选,可以用逗号分隔case的列表
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
break语句可以提前终止switch和循环语句,尽管它在Go中不似C语言中那么常见,不过有时有必要从一个环绕的循环中脱离出来,可以通过在循环上贴上标签并从标签上“断开”来跳出循环。这个例子展示了这两种用途
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
continuous语句也接受一个可选的标签,但它只适用于循环
以下是使用两个switch语句的字节切片比较例程
// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
Type Switch switch还可以用来发现接口变量的动态类型,这样的type switch使用类型断言的语法,括号内有关键字type。如果switch在表达式中声明了一个变量,则该变量在每个子句中将具有相应的类型;在这种情况下重用名称也是惯用的,实际上是用相同的名称声明一个新变量,但在每种情况下都使用不同的类型
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
函数和方法
函数 多返回值 Go的一个不寻常的特性是函数和方法可以返回多个值,带内错误返回,例如-1表示EOF,修改按地址传递的参数。在Go语言中,Write函数可以返回一个计数(写入的字节数)和一个错误,os包中Write函数的签名如下
func (file *File) Write(b []byte) (n int, err error)
类似的方法消除了将指针传递到返回值以模拟引用参数的需要。这里有一个简单的函数从字节切片中的一个位置获取一个数字,返回数字和下一个位置
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {
}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x*10 + int(b[i]) - '0'
}
return x, i
}
也可以用它像这样扫描输入切片b中的数字
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}
命名结果参数 go函数的返回或结果“参数”可以被命名并用作常规变量,就像传入的参数一样。当函数启动时,被命名的参数将被初始化为它们类型的零值;如果函数执行不带参数的返回语句,则结果参数的当前值将用作返回值。这些名称不是强制性的,但它们可以使代码更短、更清晰;如下定义一个函数netxInt的两个int类型返回参数的名称为value和nextPos
func nextInt(b []byte, pos int) (value, nextPos int) {
命名结果是初始化的,并且绑定到未修饰的返回,所以它们既可以简化,也可以声明。以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函数)安排在执行defer返回的函数之前立即运行。这是一种不寻常但有效的方法来处理这样的情况,无论函数返回的路径如何,都必须释放资源。典型的例子是解锁互斥锁或关闭文件
// Contents 以字符串形式返回文件的内容.
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.
}
推迟对close等函数的调用有两个优点:第一,它保证您永远不会忘记关闭文件,如果以后编辑函数以添加新的返回路径,则很容易犯这个错误;第二,这意味着Close位于Open附近,这比将其放在函数末尾要清楚得多
deferred函数的参数(如果函数是一种方法,则包括接收器)在defer执行时计算,而不是在调用时计算。除了避免担心变量在函数执行时更改值之外,这意味着一个延迟调用站点可以延迟多个函数的执行
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
defer函数以LIFO顺序执行,因此,上述代码返回时将会打印4 3 2 1 0
。一个更合理的例子,是通过简单的程序跟踪函数执行,我们可以编写一些简单的跟踪例程,如下所示
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....
}
我们可以更好地利用这样一个事实,即deferred函数的参数是在延迟执行时计算的。跟踪例程可以将参数设置为untracing例程。示例
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()
}
## 输出如下
entering: b
in b
entering: a
in a
leaving: a
leaving: b
方法
指针 vs. 值 正如我们在ByteSize中看到的,可以为任何命名类型(指针或接口除外)定义方法;接收器不必是结构。在上面对切片的讨论中,我们编写了一个Append函数,可以将其定义为切片上的方法。为此,首先声明一个可以绑定该方法的命名类型,然后将该方法的接收器设置为该类型的值
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// Body exactly the same as the Append function defined above.
}
这仍然需要方法返回更新后的切片,可以通过重新定义方法来消除这种冗余操作,将指向字节切片的指针作为其接收器,这样方法就可以覆盖调用方的切片
func (p *ByteSlice) Append(data []byte) {
slice := *p
// Body as above, without the return.
*p = slice
}
事实上,我们可以做得更好,如果修改我们的函数使其看起来像一个标准的write方法,就像这样
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// Again as above.
*p = slice
return len(data), nil
}
然后,类型*ByteSize满足标准io.Writer接口,例如,我们可以打印成一个
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
此处,传递ByteSize类型的地址是因为只有*ByteSize满足io.Writer。关于指针和接收器值的规则是,值方法可以在指针和值上调用,但指针方法只能在指针上调用。这一规则的产生是因为指针方法可以修改接收器;对值调用它们将导致方法接收值的副本,因此任何修改都将被丢弃。因此,该语言不允许出现此错误。不过,有一个例外:当值是可寻址的时,语言通过自动插入address运算符来处理在值上调用指针方法的常见情况。在我们的例子中,变量b是可寻址的,所以我们可以用b.Write调用它的Write方法。编译器将会为我们重写为(&b).Write。顺便提一下,在字节切片上使用Write的思想是bytes.Buffer实现的核心
数据及初始化
使用new分配 go有两个分配原语,分别是内置函数new和make,它们应用于不同的类型做不同的事情。对于new,它是一个分配内存的内置函数,并且只将其归零而不初始化内存。也就是说,new(T)为T类型的新对象分配零存储并返回它的地址,即类型*T的值。在go的术语中,它返回一个指向新分配的T类型零值指针
由于new返回的内存为零,因此在设计数据结构时,最好安排在不进行进一步初始化的情况下使用每种类型的零值。这意味着数据结构的用户可以使用new创建一个数据结构,并获得工作权限。例如,bytes.buffer的文档声明“buffer的零值是一个空缓冲区,可以使用。”同样,sync.mutex没有显式的构造函数或init方法。相反,sync.mutex的零值被定义为未加锁的mutex
zero-value-is-useful属性可以透明工作,开旅下面的类型声明
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer类型的值也可以在分配或仅声明时立即使用。在以下代码片段中,p和v都将正常工作,无需进一步安排
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
构造器和文字复合 有时零值不够好,需要初始化构造函数,如本例中从包os派生的File
func NewFile(fd int, 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
}
可以使用复合文本(composite literal)来简化它,复合文本是一个表达式,它在每次求值时都创建一个新实例
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
与C不同,返回局部变量的地址是完全可以的;与变量相关联的存储在函数返回之后仍然存在。事实上,每次计算复合文本的地址时,它都会分配一个新的实例,因此我们可以合并最后两行
return &File{fd, name, nil, 0}
作为一个限制条件,如果一个文本组合根本不包含字段,它将为类型创建一个零值。表达式new(File)
和&File{}
是等价的。还可以为数组、切片和映射创建复合文本,字段标签是索引或映射键(视情况而定),在这些示例中,不管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)
,它只用于创建slices、maps和channels并且返回一个已经初始化的(非零的)类型为T(不是*T)的值;其中,差别的原因是这三种类型存在对底层数据结构的引用,这些数据结构必须在使用之前初始化。例如,切片数据结构包括:指向数据(数组内部)、长度和容量的指针,在初始化这些项之前,切片是nil。对于切片、maps和channels,make初始化其内部数据结构,并准备要使用的值。例如
make([]int, 10, 100)
分配一个100个整数的数组,然后创建一个长度为10、容量为100的切片结构,指向数组的前面第10个元素。相反,new([]int)返回一个指向新分配的零切片结构的指针,即指向nil切片值的指针。这些例子说明了new和make之间的区别
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
Tips *make只适用于maps、slices和channels,不返回指针;使用new获取显式指针分配或显示获取变量地址
数组 数组在规划内存的详细布局时很有用,有时可以帮助避免分配,但它们主要是用于切片的构建块。数组在Go中工作方式与C有很大的不同,主要表现在
数组是值,将一个数组分配给其他数组将复制所有元素
如果将数组传递给函数,它将收到数组的副本,而不是指向它的指针
-
数组的大小是其类型的一部分,类型[10]int和[20]int是不同的
value属性可能很有用,但也很昂贵;如果要类似C的行为和效率,可以将指针传递给数组
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // 注意运算符的显示地址,即使是这种方式也不是Go惯用的方法,slice才是
Slices 切片包装数组,为数据序列提供更通用、更强大、更方便的接口。除了具有显式维度的项(如转换矩阵)外,go中的大多数数组编程都是用切片而不是简单数组完成的。切片保存对底层数组的引用,如果将切片分配给另一个切片,则两个切片都引用同一个数组。如果一个函数接受一个slice参数,它对slice元素所做的更改将对调用者可见,类似于将指针传递给底层数组。因此,Read函数可以接受slice参数,而不是指针和计数;slice中的长度设置了要读取多少数据的上限。下面是包os中文件类型的Read方法的函数签名
func (f *File) Read(buf []byte) (n int, err error)
该方法返回读取的字节数和错误值(如果有),要读入较大缓冲区的前32个字节,请分割缓冲区
n, err := f.Read(buf[0:32])
这样的切片是常见而有效的。事实上,暂时不考虑效率,下面的代码片段还将读取缓冲区的前32个字节
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
一个切片的长度可以改变,只要它仍然在底层数组的限制范围内;只需将它分配给自身的一个切片。切片的容量由内置函数cap访问,报告切片可能假定的最大长度。如下示例是一个Append函数,用于将数据追加到切片: 如果数据超出容量,则重新分配切片,返回结果切片
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
// 此处必须返回append之后ded切片,因为虽然append可以修改切片的元素,但切片本身(包含指针、长度和容量的运行时数据结构)是按值传递的
return slice
}
二维切片 go的数组和切片是一维的,要创建与二维数组或切片等效的数组或切片,需要定义array-of-arrays或slice-of-slices,如下所示
type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
切片是可变长度的,所以可以让每个内部切片具有不同的长度。这可能是一种常见的情况,如我们的LinesOfText示例:每一行都有独立的长度
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
有时候需要申请一个二维切片,例如,scan在处理像素时可能出现这种情况。有两种方式实现:其一,独立地申请每一个切片;另一种是申请一个数组并将各个切片指向其中;具体使用哪种方式取决于应用程序。如果切片可能会增长或收缩,则应单独分配它们,以避免覆盖下一行;如果没有,则可以更有效地使用单个分配构造对象。以下是这两种方法的示意图,以供参考。首先,一次一行
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize)
}
方法二:申请一个大的slice并划分为几行
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Maps map是一种方便而强大的内置数据结构,它将一种类型(键)的值与另一种类型(元素或值)的值相关联。键可以是定义了相等运算符的任何类型,例如整数、浮点和复数、字符串、指针、接口(只要动态类型支持相等)、结构和数组;切片不能用作map的键,因为它们没有定义相等性。与切片一样,map保存对底层数据结构的引用: 如果将map传递给更改map内容的函数,则这些更改将在调用方中可见
可以使用通常的复合文本语法和冒号分隔的键值对来构造,因此在初始化期间很容易构建它们
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
分配和获取map值在语法上与对数组和切片执行相同,只是索引不需要是整数
offset := timeZone["EST"]
使用map中不存在的键获取map值时,将返回map中item类型的零值。例如,如果map包含整数,查找不存在的键将返回0; 集合可以实现为值类型为bool的映射,将map item设置为true以将值放入集合中,然后通过简单的索引对其进行测试
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}
if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}
有时需要区分缺少的条目和零值。是否存在“utc”的条目?或者是0因为它根本不在map中,你可以用多重分配的形式来区分
var seconds int
var ok bool
seconds, ok = timeZone[tz]
这被称为"comma-ok"习语,在这个例子中,如果TZ存在seconds将被适当设置,ok将为true;否则,seconds被设置为零值,ok将为false。下面是一个函数,它将与一个error report放在一起
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
在不关心实际值的情况下测试map中是否存在,可以使用空白标识符代替值的常用变量
_, present := timeZone[tz]
要删除map条目,请使用delete内置函数,该函数的参数是要删除的map和key。即使key已经不在map上,这样做也是安全的
delete(timeZone, "PDT") // Now on Standard Time
Printing go中的格式化打印使用了类似于C的printf系列的样式,但更加丰富和通用。这些函数位于fmt包中,并有大写的名称:fmt.Printf、fmt.Fpprintf、fmt.Sprintf等等。字符串函数(sprintf等)返回字符串,而不是填充提供的缓冲区。不需要提供格式字符串,对于Printf、Fprintf和Sprintf,都有另一对函数,例如Print和Println。这些函数不接受格式字符串,而是为每个参数生成默认格式。Println版本也在参数之间插入一个空格,并在输出中追加一个换行符,而Print版本只在两边的操作数都不是字符串时才添加空格。在本例中,每一行产生相同的输出
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
格式化的打印函数fmt.Fprint和friends将实现io.Writer接口的任何对象作为第一个参数;变量os.Stdout和os.Stderr是常见的实例。更多细节此处不一一赘述
Append 内置Append函数的签名与上面自定义的append函数不同,其定义如下
func append(slice []T, elements ...T) []T
此处,T是任何给定类型的占位符。在go中,T类型由调用方决定,这就是append内置的原因:它需要编译器的支持。append的作用是将元素附加到切片的末尾并返回结果。需要返回结果,因为与手工编写的append一样,底层数组可能会更改。这个简单的例子
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
## 打印结果[1 2 3 4 5 6],所以append的工作方式有点像printf,收集任意数量的参数
Initialization 虽然看起来与C或C++中的初始化没有太大的区别,但Go中的初始化功能更强大。在初始化过程中可以构建复杂的结构,并且正确地处理初始化对象之间的排序问题,甚至不同包之间的排序问题
Constants Go中的常量就是常数,在编译时创建,即使在函数中定义为局部变量,也只能是数字、字符(rune)、字符串或布尔。由于编译时的限制,定义它们的表达式必须是常量表达式,由编译器计算,例如,1<<3是一个常量表达式,而math.Sin(math.Pi/4)不是,因为函数调用math.Sin发生在运行时
在go中,使用iota枚举器创建枚举常量,由于iota可以是表达式的一部分,并且表达式可以隐式重复,因此很容易构建复杂的值集
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
将诸如string之类的方法附加到任何用户定义类型的能力使任意值能够自动格式化以供打印。尽管您将看到它最常应用于结构,但此技术对于标量类型(如ByteSize等浮点类型)也很有用
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
Variables 变量可以像常量一样初始化,但初始化器可以是在运行时计算的通用表达式
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
init函数 每个源文件都可以定义init函数来设置所需的任何状态(事实上,每一个文件可以有多个init函数)。init是在包中的所有变量声明都计算了它们的初始值设定项之后调用的,而那些变量声明是在所有导入的包都初始化之后才计算的。除了不能表示为声明的初始化之外,init函数的一个常见用法是在实际执行开始之前验证或修复程序状态的正确性
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
接口及其他类型
接口 go中的接口提供了一种指定对象行为的方法。在go代码中,只有一个或两个方法的接口是常见的,通常会被赋予一个从该方法派生的名称,例如io.Writer用于实现Write的对象
一个类型可以实现多个接口。例如,一个集合实现了sort.Interface(包含Len()、Less(i, j int) bool和Swap(i, j int)),那么它可以按包sort中的例程进行排序,并且它还可以有一个自定义格式化程序。在这个虚构的例子中,Sequence同时满足条件
type Sequence []int
// Methods required by sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
Conversions Sequence的String方法正在重新创建Sprint已经为切片所做的工作(它具有O(N*N)时间复杂度,性能很差)。如果在调用Sprint之前将序列转换为[]int,我们就可以共享工作(也可以加快工作速度)
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}
此方法是转换技术的另一个示例,用于从String方法安全地调用Sprintf。因为两个类型(Sequence和[]int)是相同的,如果我们忽略类型名,那么在它们之间进行转换是合法的。转换不会创建新值,它只是临时地充当现有值具有新类型的角色。(还有其他合法的转换,例如从整数到浮点的转换,这里会创建新值)
go程序中的一个习惯用法是将表达式的类型转换为访问一组不同的方法。例如,我们可以使用现有的sort.Intslice类型将整个示例缩减为
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
我们不再让Sequence实现多个接口(排序和打印),而是使用将数据项转换为多个类型(Sequence、sort.Intslice和[]int)的能力,每个类型都有效地完成部分工作
Interface conversions and type assertions Type switches是转换的一种形式:它们接受一个接口,对于switch中的每一种情况,在某种意义上都将其转换为该情况下的类型。下面是fmt.Printf下的代码:使用Type Switches将值转换为字符串的简化版本。如果它已经是一个字符串,接口返回实际的字符串值;而如果它有一个字符串方法,调用该方法的结果
type Stringer interface {
String() string
}
var value interface{} // 调用方提供的值
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
第一种情况找到一个具体的值;第二种情况将接口转换为另一个接口
如果我们只关心一种类型呢?如果我们知道这个值包含一个字符串,我们只想提取它?one-case的type switch可以实现,类型断言也可以。类型断言接受接口值,并从中提取指定显式类型的值
value.(typeName)
结果是一个typeName类型的新值,该类型必须是具体的接口类型,或者值可以转换为第二个接口类型。要提取值中的字符串,我们可以编写
str := value.(string)
如果发现结果不包含字符串,会造成程序崩溃并出现运行时错误。这里使用"comma, ok"来安全地测试值是否是字符串
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
如果类型断言失败,str将仍然存在并为string类型,只是它将具有零值,即空字符串
Generality 如果某类型的存在只为实现一个接口,并且不会在该接口之外存在导出方法,那么该类型自身也不需要被导出。仅导出接口可以清楚地表明,除了接口中描述的内容之外,它没有任何有趣的行为。在这种情况下,构造函数应该返回一个接口值,而不是实现类型。例如,在hash库中,crc32.NewIEEE和adler32.New都返回接口类型hash.Hash32。用CRC-32算法替换GO程序中的Adler-32只需要更改构造函数调用;其余代码不受算法更改的影响
类似的方法允许不同密码包中的流密码算法与它们链接在一起的块密码分离。crypto/cipher包中的块接口指定块密码的行为,它提供对单个数据块的加密。然后,通过与bufio包的类比,实现这个接口的密码包可以用来构造流式密码,用流接口表示,而不需要知道块加密的细节。crypto/cipher形如
type Block interface {
BlockSize() int
Encrypt(dst, src []byte)
Decrypt(dst, src []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
// 这里是counter mode(CTR)stream 的定义,它将分组密码转换为流密码;请注意,分组密码的详细实现被抽象出来了
// NewCTR不仅适用于一个特定的加密算法和数据源,还适用于块接口和任何流的任何实现。因为它们返回接口值,所以用其他加密模式替换CTR加密是一种本地化的更改
// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream
Interfaces and methods 几乎任意类型都可以附加方法,几乎任何类型都可以实现接口。以http包为例,它定义了handler接口,任何实现handler的对象都可以为http请求提供服务
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter本身就是一个接口,它提供将响应返回给客户端所需方法的访问。这些方法包括标准的Write方法,因此可以在可以使用io.Writer的任何地方使用http.ResponseWriter。Request是一个结构,包含来自客户端的请求的解析表示
为了简洁起见,让我们忽略POST并假设http请求总是GET方式;这种简化不会影响handler的设置方式。下面是一个简单但完整的处理程序实现,用于计算访问页面的次数
// Simple counter server.
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
// 作为参考,将Counter服务附加到url树上
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
为什么要把计数器变成一个结构呢?只需要一个整数(接收器需要是指针,以便调用者可以看到增量)
// Simpler counter server.
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
如果您的程序有一些内部状态,需要通知您访问了某个页面,该如何处理?通过将channel绑定到web页面来实现
// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
假设我们想在args上显示调用服务二进制文件时使用的参数。可以很容易地编写一个函数来打印参数
func ArgServer() {
fmt.Println(os.Args)
}
// 如何将其转换为http服务器?我们可以让ArgServer成为某种类型的方法,忽略它的值,但有一种更简洁的方法
// 可以为除了指针和接口之外的任何类型定义方法,所以我们可以为函数编写方法。http包中包含此代码
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
// HandlerFunc是一个带有ServeHTTP方法的类型,因此该类型的值可以服务于http请求
// 看看方法的实现:接收器是一个函数f,方法调用f。这可能看起来很奇怪,但与接收器是一个通道和在通道上发送的方法没有太大区别
// 要使ArgServer成为一个http服务,我们首先修改其函数签名
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
// AargServer现在与HandlerFunc具有相同的函数签名,因此可以将其转换为该类型以访问其方法
http.Handle("/args", http.HandlerFunc(ArgServer))
// 当有人访问/args页面时,安装在该页面上的handler具有值ArgServer和类型HandlerFunc
// http服务将调用该类型的方法ServeHTTP,以ArgServer作为接收器,后者将依次调用ArgServer(通过HandlerFunc.ServeHTTP中的调用f(w,req))。然后显示参数
在本节中,从一个结构、一个整数、一个channel和一个函数创建了一个http服务,这是因为接口只是一组方法,(几乎)可以为任何类型定义
嵌入 go不提供典型的、类型驱动的子类概念,但它确实能够通过在结构或接口中嵌入类型来“借用”已实现的片段。接口嵌入非常简单。我们之前提到过io.Reader和io.Writer接口,下面是它们的定义
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
io包还导出其他几个接口,这些接口指定可以实现多个此类方法的对象。例如,有io.ReadWriter,一个同时包含读和写的接口。我们可以通过显式地列出这两个方法来指定io.ReadWriter,但是嵌入这两个接口以形成新的接口更容易引起注意,如下所示
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
ReadWriter可以做Reader和Writer做的事情;它是嵌入式接口(必须是不相交的方法集)的并集;接口中只能嵌入接口。同样的基本思想也适用于struct,bufio包有两种结构类型:bufio.Reader和bufio.Writer,当然每种类型都实现io包中类似的接口。bufio还实现了一个缓冲reader/writer,它通过使用嵌入将读写器组合到一个结构中来实现:它列出结构中的类型,但不给它们提供字段名
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
嵌入的元素是指向结构的指针,当然,在使用它们之前必须初始化为指向有效的结构。ReadWriter结构可以写成
type ReadWriter struct {
reader *Reader
writer *Writer
}
为了提升字段的方法并满足IO接口,我们还需要提供转发方法,如下所示
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
嵌入类型的方法是默认提供的,这意味着bufio.ReadWriter不仅具有bufio.Reader和bufio.Writer方法,而且还满足所有三个接口:io.Reader、io.Writer和io.ReadWriter
嵌入和子类有一个重要的区别 :当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但是当调用它们时,方法的接收者是内部类型,而不是外部类型。在我们的示例中,当调用bufio.ReadWriter的Read方法时,它的效果与上面写的转发方法完全相同;接收方是ReadWriter的Reader字段,而不是ReadWriter本身
这个例子显示了一个嵌入的字段和一个常规的、命名的字段
type Job struct {
Command string
*log.Logger
}
Job类型现在具有Print、Printf、Println和*log.Logger的其他方法。当然也可以给Logger一个字段名,但不必这样做。现在一旦初始化,我们就可以为job记录日志
job.Println("starting now...")
Logger是Job结构的常规字段,因此我们可以在Job的构造函数中以常用的方式初始化它,如下所示
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
// 或者,以符合文本方式初始化
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
空白标识符
空白标识符可以使用任何类型的值进行分配或声明,并且该值将被无害地丢弃(这有点像对Unix /dev/null
文件的写入)。它表示一个只写的值,在需要变量但不关心实际值的地方使用一个占位符,具有非常好的用途
多重赋值中的空白标识符 在for-range循环中使用空白标识符是一般情况下的一种特殊场景:多重赋值。如果一个赋值要求在左边有多个值,但其中一个值不被程序使用,赋值左边的空白标识符可以避免创建一个伪变量,并清楚地表明该值将被丢弃。例如,当调用返回值和error的函数时,如果只有error是重要的,则使用空标识符丢弃不关心的值
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
有时您会看到为了忽略错误而丢弃错误值的代码;这是一种糟糕的做法
// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}
未使用的导入和变量 导入包或声明变量而不使用会发生编译错误。未使用的导入会使程序膨胀并导致编译缓慢,而一个已初始化但未使用的变量至少是一个浪费的计算,并且可能存在潜在bug。然而,当程序处于开发阶段时,经常会出现未使用的导入和变量,为了让编译继续运行而删除它们,但如果后续再次需要它们...就比较繁琐。空白标识符提供了一种解决方法
这个半成品程序有两个未使用的导入(fmt和io)和一个未使用的变量(fd),看起来代码正确无误,实际上它是不可编译的
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
}
若要消除有关未使用导入的报错,请使用空白标识符引用导入包中的符号。类似地,将未使用的变量fd分配给空白标识符将使未使用的变量错误消失。这个版本的程序是可编译的
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader // For debugging; delete when done.
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}
按照惯例,导入错误的全局声明应该在导入后立即发布并进行注释,这样既便于查找,又可以提醒以后清理
导入的副作用 上一个例子中未使用的导入(如fmt或io)最终应该被使用或删除:空白赋值将代码标识为正在进行的工作。但有时导入一个包仅仅是为了它的副作用,而没有任何显式的使用是有用的。例如,在其init函数期间,为http handler提供调试信息而注册net/http/pprof
包。它有一个导出的api,但是大多数客户机只需要注册handler并通过web页面访问数据。若导入包仅为获取其副作用,请将包重命名为空白标识符
import _ "net/http/pprof" // 这种形式的导入清楚地表明,导入包是因为它的副作用,因为包没有其他可能的用途:在这个文件中,它没有名称
接口检查 正如我们在上面的接口讨论中看到的,类型不需要显式声明它实现了接口。相反,类型只是通过实现接口的方法来实现接口。实际上,大多数接口转换都是静态的,因此在编译时进行检查。例如,将os.File传递给需要io的函数。除非os.File实现io.Reader接口,否则不会被编译
一些接口检查发生在运行时,例如,在encoding/json包中,它定义了一个Marshall接口。当JSON编码器接收到实现该接口的值时,编码器调用该值的处理方法将其转换为JSON,而不是执行标准转换。编码器在运行时使用如下断言类型检查此属性
m, ok := val.(json.Marshaler)
如果只需要询问类型是否实现了接口,而不实际使用接口本身(可能是错误检查的一部分),则使用空标识符忽略类型断言的值
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
并发
共享通信 并发编程是一个很大的主题,这里只涉及特定于Golang的亮点。许多环境中的并发编程由于实现对共享变量的正确访问所需的微妙之处而变得困难。go鼓励采用一种不同的方法,在channel上传递共享值,事实上,从来没有被单独的执行线程主动共享过。在任何给定时间,只有一个goroutine可以访问该值。按照设计,不会发生数据竞争。为了鼓励这种思维方式,我们把它简化为一个口号
不要通过共享内存进行通信;而是通过通信共享内存
思考这个模型的一种方法是考虑在一个CPU上运行一个典型的单线程程序,它不需要同步原语。现在运行另一个这样的实例;它也不需要同步。现在让这两个通信;如果通信是同步地,则仍然不需要其他同步。例如,unix管道非常适合这个模型。尽管go的并发性方法起源于Hoare's的通信顺序进程(Communication Sequential Processes,CSP),但它也可以看作是Unix管道的一种类型安全的泛化
**Gorutines ** 它们之所以被称为Goroutines,是因为现有的术语(线程、协程、进程等)传递了不准确的含义。Goroutine有一个简单的模型:它是一个与同一地址空间中的其他Goroutine同时执行的函数。它是轻量级的,只需分配堆栈空间。而且堆栈开始很小,所以很便利,并且通过根据需要分配(和释放)堆存储来增长
goroutine被多路复用到多个OS线程上,因此如果其中一个线程被阻塞,例如在等待i/o时,其他线程将继续运行,它们的设计隐藏了线程创建和管理的许多复杂性。在函数或方法调用前面加上go关键字,创建新的goroutine运行调用。当调用完成时,goroutine将默默地退出(效果类似于Unix Shell在后台运行命令的符号&)
go list.Sort() // run list.Sort concurrently; don't wait for it.
在goroutine调用中,文本函数非常方便
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // Note the parentheses - must call the function.
}
Channels 与映射类似,channel也使用make进行分配,结果值作为对底层数据结构的引用。如果提供了可选的整数参数,表示设置通道的缓冲区大小;对于未设置缓存区大小或同步通道,默认值为零
ci := make(chan int) // unbuffered channel of integers
cj := make(chan int, 0) // unbuffered channel of integers
cs := make(chan *os.File, 100) // buffered channel of pointers to Files
无缓冲channel将通信(值的交换)与同步结合起来,保证两个计算(goroutine)处于Ready状态
使用channel有很多很好的习惯用法。在上一节中,我们在后台启动了排序,channel可以允许启动goroutine等待排序完成
c := make(chan int) // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
list.Sort()
c <- 1 // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c // Wait for sort to finish; discard sent value.
Receiver总是处于阻塞状态直到有数据要接收。如果不是缓冲channel,则发送方阻塞,直到接收器接收到该值。如果是带缓冲的channel,则发送方仅在值复制到缓冲区之前阻塞;如果缓冲区已满,则意味着要等待某个接收器检索到值
带缓冲的channels可以像信号量一样使用,例如限制吞吐量。在本例中,传入的请求被传递到handle,handle将值发送到channel,处理请求,然后从channel接收值,以便为下一个使用者准备信号量。channel缓冲区的容量限制了要处理的同时调用的数量
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // Wait for active queue to drain.
process(r) // May take a long time.
<-sem // Done; enable next request to run.
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Don't wait for handle to finish.
}
}
一旦handlers正在执行进程达到MaxOutstanding,将阻止尝试发送到channel的缓冲区,直到现有处理程序完成退出然后从缓冲区接收下一个信号。不过,这种设计有一个问题:Server为每个传入的请求创建一个新的goroutine,即使MaxUnstanding可以在任何时候运行。如果请求太快,程序可以消耗无限的资源。我们可以通过将Server更改为goroutines的创建gate来解决这个缺陷。这是一个显而易见的解决方案,但请注意它有一个我们稍后将修复的错误
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // Buggy; see explanation below.
<-sem
}()
}
}
bug在于,在go的for循环中,循环变量在每次迭代中都被重用,因此req变量在所有goroutine中共享...这不是我们想要的。我们需要确保每个goroutine的req是唯一的。有一种方法可以做到这一点,将req的值作为参数传递给goroutine中的闭包
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
与前一版本进行比较,以查看如何声明和运行闭包;另一个解决方案是创建一个同名的新变量,如本例所示
func Serve(queue chan *Request) {
for req := range queue {
req := req // Create new instance of req for the goroutine.
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
// req := req 看起来很奇怪,这是合法的也是处理这种问题的惯用方法:获得同名变量的新版本,故意在本地隐藏循环变量,但对每个goroutine都是唯一的
回到编写服务器的一般问题,管理好资源的另一种方法是启动固定数量的handle goroutines,从请求channels读取所有内容。goroutines的数量限制了同时调用进程的次数。这个服务函数还接受一个channel,在这个channel上,它将被告知退出;在启动goroutines之后,它将阻止从该channel接收
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Start handlers
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Wait to be told to exit.
}
Channels of channels go最重要的特性之一是channel是first-class值,可以像其他任何channel一样分配和传递。此属性的一个常见用途是实现安全的并行解复用。在上一节的示例中,handle是请求的理想处理程序,但我们没有定义它正在处理的类型。如果该类型包含Response channel,则每个客户端都可以为Response提供自己的路径。下面是请求类型的示意定义
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
客户端提供一个函数及其参数,以及请求对象内部的channel,用于接受Response
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)
在服务器端,仅需改变handler函数即可。显然还要做很多事情才能实现它,但这段代码是一个用于速率受限、并行、非阻塞的RPC系统的框架,而且看不到互斥锁
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
Parallelization 这些思想的另一个应用是跨多个CPU核并行计算。如果可以将计算分解成可以独立执行的单独部分,则可以将其并行化,并在每个部分完成时提供一个信号通道
假设我们有一个昂贵的操作来执行向量上的项目,并且每个项的操作值都是独立的,这是个理想化的例子
type Vector []float64
// Apply the operation to v[i], v[i+1] ... up to v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // signal that this piece is done
}
在循环中独立地启动这些部件,每个核一个。它们可以按任何顺序完成,但这无关紧要;我们只需在启动所有goroutine之后通过draining通道来计算完成信号
const numCPU = 4 // number of CPU cores
func (v Vector) DoAll(u Vector) {
c := make(chan int, numCPU) // Buffering optional but sensible.
for i := 0; i < numCPU; i++ {
go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
}
// Drain the channel.
for i := 0; i < numCPU; i++ {
<-c // wait for one task to complete
}
// All done.
}
运行时核数的设置,可以不必硬编码,通过runtime.NumCPU来设置机器中硬件实际的CPU数量
var numCPU = runtime.NumCPU()
还有一个函数runtime.GOMAXPROCS,它报告(或设置)go程序可以同时运行用户指定核数。它默认为runtime.NumCPU值,但可以通过设置名称类似的Shell环境变量或通过使用正数调用函数来覆盖默认值,用零调用它只是查询值。因此,如果我们想满足用户的资源请求,我们应该写
var numCPU = runtime.GOMAXPROCS(0)
缓冲 并发编程的工具也可以使非并发性的想法更容易表达。下面是一个从RPC包中抽象出来的示例。客户端goroutine循环从某个源(可能是网络)接收数据。为了避免分配和释放缓冲区,它保留一个空闲list,并使用一个缓冲通道来表示它。如果通道为空,则分配新的缓冲区。一旦消息缓冲区准备好,它就通过serverChan发送到服务器上
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)
func client() {
for {
var b *Buffer
// Grab a buffer if available; allocate if not.
select {
case b = <-freeList:
// Got one; nothing more to do.
default:
// None free, so allocate a new one.
b = new(Buffer)
}
load(b) // Read next message from the net.
serverChan <- b // Send to server.
}
}
服务端循环从客户端接收每条消息,对其进行处理,并将返回缓冲区空闲列表
func server() {
for {
b := <-serverChan // Wait for work.
process(b)
// Reuse buffer if there's room.
select {
case freeList <- b:
// Buffer on free list; nothing more to do.
default:
// Free list full, just carry on.
}
}
}
Errors
库例程必须经常向调用方返回某种错误指示。如前所述,go的多值返回使得在返回正常值的同时返回详细的错误描述变得很容易。使用此功能提供详细的错误信息是一种很好的方式。例如,正如我们将看到的,os.Open不仅在失败时返回一个nil指针,它还返回一个错误值来描述发送什么错误
错误有一个类型error,一个简单的内置接口
type error interface {
Error() string
}
库编写者可以自由地使用更丰富的模型来实现这个接口,这样不仅可以看到错误,还可以提供一些上下文。如前所述,除了通常的*os.File返回值外,os.Open还返回一个错误值。如果文件成功打开,则错误为nil,但出现问题时,它将返回一个os.PathError
// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}