Go程序设计语言(二)
本文译自Rob Pike的Go语言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语言中的隐式指针。你可以获得数组的地址,并生成一个指向数组的指针(例如,将其高效地传递给函数):
[0 0 0]
&[0 0 0]
数组字面值
指向数组字面值的指针
切片(Slice)
切片
切片速记
切片引用数组
创建切片
切片字面值看起来像没有指定大小的数组字面值:
切片容量
调整切片大小
切片使用的代价很小
Maps
maps
map的创建
map索引
测试存在性
value, ok := m[x] // "comma ok" 形式
删除
如果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}
将range用于字符串
Structs
structs
struct是值类型
创建结构体
导出类型和字段
匿名字段
一个匿名结构体字段
匿名字段以类型作为名字
任意类型的匿名字段
任何具名类型或指向具名类型的指针都可以用作匿名字段。它们可以出现在结构体中的任意位置。
冲突和遮蔽
冲突的例子
方法(method)
基于结构体的方法
基于结构体值的方法
调用一个方法
和你所期望的一样。
方法的基本规则
指针与值
同样,如果方法接收者是Point3类型,你 也可以使用一个*Point3类型的指针调用它。
有关匿名字段的方法
匿名字段例子
重写一个方法
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]。
评论