标签 Interface 下的文章

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]。

动态代理再思考

看了透明发表在《程序员》杂志2005年第一期上的“动态代理的前世今生”,让我不仅了解了“动态代理”这门技术,更让我知道了一段Java技术的发展史。带着对Rickard Oberg的钦佩之情,怀着对Rod Johnson敬仰之义我踏上了动态代理再思考之路。

[关键词]

代理(proxy)

基础设施(infrastructure)

业务组件(business component)

拦截器(interceptor)

面向方面编程(AOP)

 

千里之行,始于足下;

九层之台,起于累土

 

任何事情都不能一蹴而就,物极必反的道理相信大家都或多或少的懂一些。

动态代理是一门较高级的技术,我们自己在平时的开发中也许很少用到,但是在你使用的开源工具包中也许就有它的足迹。动态代理技术用起来简单,但是理解起来并不是那么顺畅,我们从最简单的地方开说。

我们先来看看动态代理的定义:

[Definition]

动态代理类是这样的一个类:可以在运行时、在创建这个类的时候才指定它所实现的接口。每个代理类的实例都有一个对应的InvocationHandler对象。

也许看完这个定义,第一感觉是“看了还不如不看”J

不过在你理解了动态代理之后你会体会到这句话的确很精辟。

下面是改自JDK Doc中的一个动态代理的例子,我们先来个感性认识,看例子的时候别忘了回头复习一下那个Definition,也你灵光一闪,一切都豁然开朗。

/*******************************begin******************************************/

[Demo-1]

//要代理的接口的定义

public interface BusinessIntf {

       void doSomething();

}

//用户代码

BusinessIntf b = (BusinessIntf)Proxy.newProxyInstance(BusinessIntf.class.getClassLoader ,

new Class[] { BusinessIntf.class },

handler);

b.doSomething();

/*********************************end****************************************/

观后而感之,使用动态代理就这么简单。在运行时、在Proxy实例创建时指定要代理的接口(这里的代理接口是BusinessIntf,我们要通过Proxy来获得该接口的一个实现类的实例)。除了指定代理接口之外,我们不能忘记还有个重要的参数需要传递, 那就是一个InvocationHandler接口的实现。大家一定想到了真正的业务逻辑实现一定与handler参数有关,继续探秘。

察看Doc,发现InvocationHandler下面只有这么一个方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

Doc中的英文说明太长,不看了,找一个例子看看吧。

/************************************begin************************************/

public class MyInvocationHandler implements InvocationHandler{

       private final Object target;

       public MyInvocationHandler(final Object target){

              this.target = target;

       }

       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

              try {

                     Object result = method.invoke (target, args);

                     return result;

              } catch (final InvocationTargetException e) {

                     throw e.getTargetException( );

              }

       }

};

BusinessImpl target = new BusinessImpl (); //BusinessImpl class implements the BusinessIntf

MyInvocationHandler handler = new MyInvocationHandler (target);

/***********************************end***************************************/

恍然大悟,原来真正实现业务逻辑的是传给MyInvocationHandler的一个BusinessIntf的实现。

 

也许你又陷入另一种疑惑当中,你心里可能在想这样的一个问题:只是为了获得BusinessIntf的一个实现类的实例而已,用得着使用动态代理这样高级的技术,绕个大圈子吗?像下面这样写不就可以了么

BusinessIntf b = new BusinessImpl ();

b.doSomething();

 

或者如果想写的高级一点我们可以采用静态代理,使用工厂模式

比如:

class BusinessFactory {

       //… 

public static BusinessInf getBussinessImpl() {

       return new BusinessImpl();

}

}

BusinessIntf b = BusinessFactory.getBussinessImpl();

b.doSomething();

 

我曾几何时不是这么想的。不过还是先看看下面的理由能否说服你吧。

 

[理由1] – 大师言论

《设计模式》一书中给出的理由是“我们有时需要提供一个代理来控制对这个对象(上面例子中的targetBusinessImpl的一个实例)的访问”。书中列举了几种可能使用到代理的情况:

* Remote Proxy – 隐藏对象的空间信息

* Virtual Proxy – 不常见

* Protection Proxy – 访问权限控制

* Smart Reference – 用于提供访问对象时的附加操作

 

[理由2] – 动态性

 

运行时改变 体现出其动态性

之所以称之为动态代理,就是因为该代理类的实例可实现任意的业务接口,并且可以在运行时决定一个实例究竟实现哪个接口。

从上面的代码也可以看出:

1、  我们可以在运行时改变我们要实现的接口;

2、  我们可以在运行时改变传入的InvocationHandler的实现;换句话说InvocationHandler可以创建任何接口的实例;

3、  我们可以改变在MyInvocationHandler中那个真正实现业务逻辑的对象(就是那个target)。

以上的动态性是使用静态代理较难做到的。

 

美则观之,

美则用之

经过上面的阐述,我们领略些动态代理的优势,不过我们再来看看Demo-1的用户代码,

BusinessIntf b = (BusinessIntf)Proxy.newProxyInstance(BusinessIntf.class.getClassLoader ,

new Class[] { BusinessIntf.class },

handler);

b.doSomething();

要使用BusinessIntf接口还真是不那么容易,起码我们需要自己传入handler,而handler的定义也给用户带来了很大的麻烦。

我们要明确用户究竟想要什么?

当用户写下如下代码“BusinessIntf b = ”时你会怎么想,显然用户需要的是一个BusinessIntf接口实现类的实例。而像上面的代码我们却要求用户写一些他们并不十分关心的东西,这显然不美。我们来做一下改进,使动态代理可以像静态代理那样用。

[Demo-2]

/*******************************begin******************************************/

public class BusinessProxyFactory {

       public static BusinessIntf newProxyInstance() {

              BusinessImpl target = new BusinessImpl ();

MyInvocationHandler handler = new MyInvocationHandler (target);

              return BusinessIntfProxy.newProxyInstance(BusinessIntf.class.getClassLoader() ,

new Class[] { BusinessIntf.class },

handler);

       }

}

//用户代码

BusinessIntf b =  BusinessProxyFactory.newProxyInstance();

b.doSomething();

/*******************************end******************************************/

 

轻量级容器之风行

自从PicoContainer、Spring等轻量级容器诞生后,在J2EE世界就刮起了一股“轻量级”之风。轻量级容器实现了一种“依赖注入”的机制。

以PicoContainer为例,它实现了

a) 全权管理组件的创建、生命周期和依赖关系;

b) 使用者获取组件必须通过容器,容器保证组件全局唯一访问点。 

 

我在这里对上面的代码进行“容器化改造”,使之跟上“容器之风”J 

 

// GeneralInvocationHandler.java

public interface GeneralInvocationHandler extends InvocationHandler{

       Class getImplClass();

 

//ProxyFactory.java

public class ProxyFactory {

       private GeneralInvocationHandler handler;

       public ProxyFactory(GeneralInvocationHandler handler){

              this.handler = handler;

       }

      

       public Object newProxyInstance(){

              return Proxy.newProxyInstance(handler.getImplClass().getClassLoader(),

                new Class[] { handler.getImplClass() },

                handler);

       }    

}

[Note] Demo-3设计说明(2

在类图中BusinessImplProxy实现了GeneralInvocationHandler,并依赖BusinessIntf接口,也就是说一个GeneralInvocationHandler的实现类(如BusinessImplProxy)是与一个特定的业务接口绑定的,它只能代理唯一的接口,不过选择哪个代理接口的实现类,我们可以在配置文件中在运行时指定。

//BusinessIntf.java,定义一个业务接口

public interface BusinessIntf {

       void doSomething();

 

//BusinessImplProxy.java,该类绑定了BusinessIntf接口

public class BusinessImplProxy implements GeneralInvocationHandler{

       private BusinessIntf b ;

       public BusinessImplProxy(BusinessIntf b){

              this.b = b;

       }

       public Class getImplClass() {

              return BusinessIntf.class;

       }

       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

              try {

                     Object result = method.invoke(b, args);

                     return result;

              } catch (final InvocationTargetException ex) {

                     throw ex.getTargetException( );

              }

       }

}

 

[Note] Demo-3设计说明(3

经过上面的两个说明,我们可以得出下面结论:

1、  ProxyFactory可以获取任意接口的实例,它依赖于一个绑定了特定业务接口的GeneralInvocationHandler的实现类;

2、  GeneralInvocationHandler的实现类绑定了特定的业务接口,我们可以在运行时指定具体的业务接口的实现类;

3、  所有这些我们都使用PicoContainer来进行组装,我们只需要提供配置文件。

/*******************************begin******************************************/

//Client.java ,欲使用BusinessIntf接口的Client

public class Client {

       private ProxyFactory pf;

       public Client(ProxyFactory pf){

              this.pf = pf;

       }

      

       public void run(){

              BusinessIntf b = (BusinessIntf)pf.newProxyInstance();

              b.doSomething();

       }

 

//Main.java

public class Main {

       public PicoContainer buildContainer(ScriptedContainerBuilder builder,

                     PicoContainer parentContainer, Object scope) {

              ObjectReference containerRef = new SimpleReference();

              ObjectReference parentContainerRef = new SimpleReference();

              parentContainerRef.set(parentContainer);

              builder.buildContainer(containerRef, parentContainerRef, scope, true);

              return (PicoContainer) containerRef.get();

       }

       public void startup() {

              Reader script = null;

              try {

                     script = new FileReader("nanocontainer.xml");

              } catch (FileNotFoundException fnfe) {

                     fnfe.printStackTrace();

              }

              XMLContainerBuilder builder = new XMLContainerBuilder(script,

                            getClass().getClassLoader());

              PicoContainer pico = buildContainer(builder, null, "SOME_SCOPE");

              Client c = (Client)pico.getComponentInstance(Client.class);

              c.run();

       }

      

       public static void main(String[] args){

              Main app = new Main();

              app.startup();

       }

}

 

//nanocontainer.xml  

   

   

   

      

      

 

 

/*******************************end******************************************/

 

进化,go on!, AOP

[Note]

基础设施、业务组件和用户代码三者之间的关系:

l         基础设施:包括系统的日志、安全性检查、事务管理等,这些功能的共同点    就是存在于各个业务对象的继承体系当中,任何业务对象都有可能需要它们。

l         业务组件:系统对外提供核心业务逻辑的业务对象或业务对象的集合。

l         用户代码:根据系统提供的业务接口,调用业务组件完成特定功能。

一般用户代码只和业务组件打交道,用户并不关心业务组件是否使用了和使用了哪些基础设施。在Note中也说过基础设施存在于各个业务组件中,我们来考虑这样一个问题:假设我们有业务组件business1,business2,business3,我们提供了日志和事务管理两种基础设施,开始的时候我们由于需求的原因,我们只在各个业务组件(business1—business3)中使用了日志这么一种基础设施,现在需求发生变化了,我们需要在各个组件中加入事务管理。我们怎么办?体力活,一个组件一个组件的修改。客户的需求总是在变化,也许明天又会有“添加安全性检查”的需求。现在一切都集中到了这样一个问题上:

 

[问题]

“如何不修改业务组件代码,而动态的添加和删除组件需要的基础设施”?

Interceptor(拦截器),将各个基础设施都实现为拦截器,业务组件需要哪些基础设施直接在配置文件中配置即可。而业务组件在真正执行业务前需经过一个基础设施的拦截器链的拦截。而拦截器的一个主要的实现技术就是“动态带来技术”。当然这个实现更加复杂。

了解AOP的人对上面的描述一定不会感到陌生,因为这也恰是一种AOP的思想。目前很多AOP的开源实现都是基于“动态代理”技术。著名的AOP联盟也发布了“基于动态代理的AOP框架”。如果对之感兴趣的话,可以继续深入研究。

 

参考资料

1、“动态代理的前世今生” 《程序员》 2005-01期

2、《Hardcore Java》

3、JDK Doc

4、PicoContainer/NanoContainer Doc

 

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

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

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

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

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

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats