当前位置: 代码迷 >> 综合 >> Go语言学习笔记(九)------接口(Interfaces)与反射(reflection)
  详细解决方案

Go语言学习笔记(九)------接口(Interfaces)与反射(reflection)

热度:95   发布时间:2023-12-10 18:13:27.0

一、接口定义

1.接口提供了一种方式来说明对象的行为:如果谁能搞定这件事,它就可以用在这儿。接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的),接口里也不能包含变量。通过如下格式定义接口:

type Namer interface {  // Namer 是一个接口类型
Method1(param_list) return_type
Method2(param_list) return_type
...
}

2.Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法,(按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如Printer 、 Reader 、 Writer 、 Logger  等等。还有一些不常用的方式(当后缀 er 不合适时),比如Recoverable ,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。

3.Go 语言中接口可以有值,一个接口类型的变量或一个接口值 : var ai Namer , ai 是一个多字(multiword)数据结构,它的值是 nil ,它本质上是一个指针。。实现了 Namer 接口类型的变量可以赋值给 ai (接收者值),此时方法表中的指针会指向被实现的接口方法。当然如果另一个类型(也实现了该接口)的变量被赋值给 ai ,这二者(指针和方法实现)也会随之改变。

4.类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。实现某个接口的类型(除了实现接口方法外)可以有其他的方法。一个类型可以实现多个接口。接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。所有这些特性使得接口具有很大的灵活性。

package main
import "fmt"
type Shaper interface {Area() float32
}
type Square struct {side float32
}
func (sq *Square) Area() float32 {return sq.side * sq.side
}
func (sq *Square) Zhouchang() float32{return  sq.side*4
}
func main() {sq1 := new(Square)sq1.side = 5var areaIntf ShaperareaIntf = sq1// shorter,without separate declaration:// areaIntf := Shaper(sq1)// or even:// areaIntf := sq1fmt.Printf("The square has area: %f\n,周长是:%f", areaIntf.Area(),sq1.Zhouchang())
}

5.有两个类型 stockPosition 和 car ,它们都有一个 getValue() 方法,我们可以定义一个具有此方法的接口 valuable 。接着定义一个使用 valuable 类型作为参数的函数 showValue() ,所有实现了valuable 接口的类型都可以用这个函数。

package main
import "fmt"
type stockPosition struct {ticker stringsharePrice float32count float32
}
/* method to determine the value of a stock position */
func (s stockPosition) getValue() float32 {return s.sharePrice * s.count
}
type car struct {make stringmodel stringprice float32
}
/* method to determine the value of a car */
func (c car) getValue() float32 {return c.price
}
/* contract that defines different things that have value */
type valuable interface {getValue() float32
}
func showValue(asset valuable) {fmt.Printf("Value of the asset is %f\n", asset.getValue())
}
func main() {o  :=  valuable( stockPosition{"GOOG", 577.20, 4})//var o valuable = stockPosition{"GOOG", 577.20, 4}showValue(o)o = car{"BMW", "M3", 66500}showValue(o)
}

二、接口嵌套接口

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。

type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}

三、类型断言:如何检测和转换接口变量的类型

1.一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。在执行过程中动态类型可能会有所不同,但是它总是可以分配给接口变量本身的类型。通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值:v := varI.(T) // unchecked type assertion。更安全的方式是使用以下形式来进行类型断言:

if v, ok := varI.(T); ok { // checked type assertion
Process(v)
return
}
// varI is not of type T

如果转换合法, v 是 varI 转换到类型 T 的值, ok 会是 true ;否则 v 是类型 T 的零值, ok 是 false ,也没有运行时错误发生。应该总是使用上面的方式来进行类型断言。多数情况下,我们可能只是想在 if 中测试一下 ok 的值,此时使用以下的方法会是最方便的:

if _, ok := varI.(T); ok {
// ...
}

2.程序中定义了一个新类型 Circle ,它也实现了 Shaper 接口。 if t, ok := areaIntf.(*Square); ok 测试 areaIntf里是否有一个包含 *Square 类型的变量,结果是确定的;然后我们测试它是否包含一个 *Circle 类型的变量,结果是否定的。

package main
import ("fmt""math"
)
type Square struct {side float32
}
type Circle struct {radius float32
}
type Shaper interface {Area() float32
}
func main() {var areaIntf Shapersq1 := new(Square)sq1.side = 5areaIntf = sq1// Is Square the type of areaIntf?if t, ok := areaIntf.(*Square); ok {fmt.Printf("The type of areaIntf is: %T\n", t)}if u, ok := areaIntf.(*Circle); ok {fmt.Printf("The type of areaIntf is: %T\n", u)} else {fmt.Println("areaIntf does not contain a variable of type Circle")}
}
func (sq *Square) Area() float32 {return sq.side * sq.side
}
func (ci *Circle) Area() float32 {return ci.radius * ci.radius * math.Pi
}

四、类型判断:type-switch

1.接口变量的类型也可以使用一种特殊形式的 switch 来检测:type-switch。变量 t 得到了 areaIntf 的值和类型, 所有 case 语句中列举的类型( nil 除外)都必须实现对应的接口(在上例中即 Shaper ),如果被检测类型没有在 case 语句列举的类型中,就会执行 default 语句。可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough 。如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句,比如:

switch areaIntf.(type) {
case *Square:
// TODO
case *Circle:
// TODO
...
default:
// TODO
}

2.代码片段展示了一个类型分类函数,它有一个可变长度参数,可以是任意类型的数组,它会根据数组元素的实际类型执行不同的动作:

func classifier(items ...interface{}) {
for i, x := range items {
switch x.(type) {
case bool:
fmt.Printf("Param #%d is a bool\n", i)
case float64:
fmt.Printf("Param #%d is a float64\n", i)
case int, int64:
fmt.Printf("Param #%d is a int\n", i)
case nil:
fmt.Printf("Param #%d is a nil\n", i)
case string:
fmt.Printf("Param #%d is a string\n", i)
default:
fmt.Printf("Param #%d is unknown\n", i)
}
}
}

可以这样调用此方法: classifier(13, -14.3, "BELGIUM", complex(1, 2), nil, false) 。在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。

五、测试一个值是否实现了某个接口

假定 v 是一个值,然后我们想测试它是否实现了 Stringer 接口,可以这样做:

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

Print 函数就是如此检测类型是否可以打印自身的。接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。编写参数是接口变量的函数,这使得它们更具有一般性。使用接口使代码更具有普适性。

六、使用方法集与接口

作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂,原因是接口变量中存储的具体值是不可寻址的在 lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender ,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的,因为 Len 定义在值上。在 plst 上调用 CountInto 是可以的,因为 CountInto 需要Appender ,并且它的方法 Append 定义在指针上。在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。

package main
import ("fmt"
)
type List []int
func (l List) Len() int {return len(l)
}
func (l *List) Append(val int) {*l = append(*l, val)
}
type Appender interface {Append(int)
}
func CountInto(a Appender, start, end int) {for i := start; i <= end; i++ {a.Append(i)}
}
type Lener interface {Len() int
}
func LongEnough(l Lener) bool {return l.Len()*10 > 42
}
func main() {// A bare valuevar lst List// compiler error:// cannot use lst (type List) as type Appender in argument to CountInto:// List does not implement Appender (Append method has pointer receiver)// CountInto(lst, 1, 10)if LongEnough(lst) { // VALID:Identical receiver typefmt.Printf("- lst is long enough\n")}// A pointer valueplst := new(List)CountInto(plst, 1, 10) //VALID:Identical receiver typeif LongEnough(plst) {// VALID: a *List can be dereferenced for the receiverfmt.Printf("- plst is long enough\n")}
}

总结:在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 P 直接可以辨识的:指针方法可以通过指针调用、值方法可以通过值调用、接收者是值的方法可以通过指针调用,因为指针会首先被解引用、接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址、将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。
注:Go 语言规范定义了接口方法集的调用规则:类型 T 的可调用方法集包含接受者为 T 或 T 的所有方法集、类型 T 的可调用方法集包含接受者为 T 的所有方法、类型 T 的可调用方法集不包含接受者为 *T 的方法。

七、 第一个例子:使用 Sorter 接口排序

例子是来自标准库的 sort 包,要对一组数字或字符串排序,只需要实现三个方法:反映元素个数的Len() 方法、比较第 i 和 j 个元素的 Less(i, j) 方法以及交换第 i 和 j 个元素的 Swap(i, j) 方法。排序函数的算法只会使用到这三个方法(可以使用任何排序算法来实现,此处我们使用冒泡排序):

//sort.go
package sort
type Sorter interface {Len() int           //长度Less(i, j int) bool //比较Swap(i, j int) //交换
}
func Sort(data Sorter) {   //冒泡排序从大到小for pass := 1; pass < data.Len(); pass++ { //从第一个开始for i := 0; i < data.Len()-pass; i++ {if data.Less(i+1, i) {data.Swap(i, i+1)}}}
}
func IsSorted(data Sorter) bool {   //检查是否以排序,从大到小n := data.Len()for i := n - 1; i > 0; i-- {if data.Less(i, i-1) {return false}}return true
}
// Convenience types for common cases
type IntArray []int                                          //针对int切片
func (p IntArray) Len() int { return len(p) }               //返回int长度
func (p IntArray) Less(i, j int) bool { return p[i] < p[j] } //索引对应的两个值大小布尔值
func (p IntArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }  //交换两值
type StringArray []string
func (p StringArray) Len() int { return len(p) }
func (p StringArray) Less(i, j int) bool { return p[i] < p[j] }
func (p StringArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// Convenience wrappers for common cases
func SortInts(a []int) { Sort(IntArray(a)) } //int切片排序
func SortStrings(a []string) { Sort(StringArray(a)) }
func IntsAreSorted(a []int) bool { return IsSorted(IntArray(a)) }  //检查是否排序
func StringsAreSorted(a []string) bool { return IsSorted(StringArray(a)) }

函数 Sort(data Interface) 用来对此类对象进行排序,可以用它们来实现对其他类型的数据(非基本类型)进行排序。在例子中,我们也是这么做的,不仅可以对 int 和 string 序列进行排序,也可以对用户自定义类型 dayArray 进行排序。

//sortmain.go
package sort
import ("./sort""fmt"
)
func ints() {data := []int{74, 59, 238, -784, 9845, 959, 905, 0, 0, 42, 7586, -5467984, 7586}a := sort.IntArray(data) //conversion to type IntArraysort.Sort(a)if !sort.IsSorted(a) {panic("fails")}fmt.Printf("The sorted array is: %v\n", a)
}
func strings() {data := []string{"monday", "friday", "tuesday", "wednesday", "sunday", "thursday", "", "saturday"}a := sort.StringArray(data)sort.Sort(a)if !sort.IsSorted(a) {panic("fail")}fmt.Printf("The sorted array is: %v\n", a)
}
type day struct {num intshortName stringlongName string
}
type dayArray struct {data []*day
}
func (p *dayArray) Len() int { return len(p.data) }
func (p *dayArray) Less(i, j int) bool { return p.data[i].num < p.data[j].num }
func (p *dayArray) Swap(i, j int) { p.data[i], p.data[j] = p.data[j], p.data[i] }
func days() {Sunday := day{0, "SUN", "Sunday"}Monday := day{1, "MON", "Monday"}Tuesday := day{2, "TUE", "Tuesday"}Wednesday := day{3, "WED", "Wednesday"}Thursday := day{4, "THU", "Thursday"}Friday := day{5, "FRI", "Friday"}Saturday := day{6, "SAT", "Saturday"}data := []*day{&Tuesday, &Thursday, &Wednesday, &Sunday, &Monday, &Friday, &Saturday}a := dayArray{data}sort.Sort(&a)if !sort.IsSorted(&a) {panic("fail")}for _, d := range data {fmt.Printf("%s ", d.longName)}fmt.Printf("\n")
}
func main() {ints()strings()days()
}

八、第二个例子:读和写

只要类型实现了读写接口,提供 Read() 和 Write 方法,就可以从它读取数据,或向它写入数据。一个对象要是可读的,它必须实现 io.Reader 接口,这个接口只有一个签名是 Read(p []byte) (n int, err error) 的方法,它从调用它的对象上读取数据,并把读到的数据放入参数中的字节切片中,然后返回读取的字节数和一个 error 对象,如果没有错误发生返回 nil ,如果已经到达输入的尾端,会返回 io.EOF("EOF") ,如果读取的过程中发生了错误,就会返回具体的错误信息。类似地,一个对象要是可写的,它必须实现 io.Writer 接口,这个接口也只有一个签名是 Write(p[]byte) (n int, err error) 的方法,它将指定字节切片中的数据写入调用它的对象里,然后返回实际写入的字节数一个 error 对象(如果没有错误发生就是 nil )。io 包里的 Readers 和 Writers 都是不带缓冲的, bufio 包里提供了对应的带缓冲的操作,在读写 UTF-8 编码的文本文件时它们尤其有用。

type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}

九、空接口

1.空接口或者最小接口 不包含任何方法,它对实现不做任何要求:type Any interface {},任何其他类型都实现了空接口, any 或 Any 是空接口一个很好的别名或缩写。可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。

package main
import "fmt"
type specialString string
var whatIsThis specialString = "hello"
func TypeSwitch() {testFunc := func(any interface{}) {switch v := any.(type) {case bool:fmt.Printf("any %v is a bool type", v)case int:fmt.Printf("any %v is an int type", v)case float32:fmt.Printf("any %v is a float32 type", v)case string:fmt.Printf("any %v is a string type", v)case specialString:fmt.Printf("any %v is a special String!", v)default:fmt.Println("unknown type!")}}testFunc(whatIsThis)
}
func main() {TypeSwitch()
}

2.构建通用类型或包含不同类型变量的数组:通过使用空接口,我们给空接口定一个别名类型 Element : type Element interface{}。然后定义一个容器类型的结构体 Vector ,它包含一个 Element 类型元素的切片:

type Vector struct {
a []Element
}

Vector 里能放任何类型的变量,因为任何类型都实现了空接口,实际上 Vector 里放的每个元素可以是不同类型的变量。我们为它定义一个 At() 方法用于返回第 i 个元素:

func (p *Vector) At(i int) Element {
return p.a[i]
}

再定一个 Set() 方法用于设置第 i 个元素的值:

func (p *Vector) Set(i int, e Element) {
p.a[i] = e
}

Vector 中存储的所有元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。

3.复制数据切片至空接口切片:必须使用 for-range 语句来一个一个显式地复制:

var dataSlice []myType = FuncReturnSlice()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}

4.通用类型的节点数据结构:节点的递归结构体类型,节点包含一个某种类型的数据字段。现在可以使用空接口作为数据字段的类型,这样我们就能写出通用的代码。下面是实现一个二叉树的部分代码:通用定义、用于创建空节点的 NewNode 方法,及设置数据的 SetData 方法。

package main
import "fmt"
type Node struct {le *Nodedata interface{}ri *Node
}
func NewNode(left, right *Node) *Node {return &Node{left, nil, right}
}
func (n *Node) SetData(data interface{}) {n.data = data
}
func main() {root := NewNode(nil, nil)root.SetData("root node")// make child (leaf) nodes:a := NewNode(nil, nil)a.SetData("left node")b := NewNode(nil, nil)b.SetData("right node")root.le = aroot.ri = bfmt.Printf("%v\n", root) // Output: &{0x125275f0 root node 0x125275e0}
}

5.接口到接口:一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 Go 语言动态的一面,可以拿它和 Ruby 和 Python 这些动态语言相比较。假定:

var ai AbsInterface // declares method Abs()
type SqrInterface interface {
Sqr() float
}
var si SqrInterface
pp := new(Point) // say *Point implements Abs, Sqr
var empty interface{}

那么下面的语句和类型断言是合法的:

empty = pp // 空接口满足一切接口
ai = empty.(AbsInterface) //接口赋值到具有相同底层方法接口
// (runtime failure otherwise)
si = ai.(SqrInterface) // 即使没有该接口方法,也会赋值sqr()
empty = si // *Point implements 是空接口
// Note: statically checkable so type assertion not necessary.

下面是函数调用的一个例子:

type myPrintInterface interface {
print()
}
func f3(x myInterface) {
x.(myPrintInterface).print() // type assertion to myPrintInterface
}

x 转换为 myPrintInterface 类型是完全动态的:只要 x 的底层类型(动态类型)定义了 print 方法这个调用就可以正常运行。

十、 反射包

1.方法和类型的反射:反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如它的大小、方法和动态的调用这些方法。

2.变量的最基本信息就是类型和值:反射包的 Type 用来表示一个 Go 类型,反射包的 Value 为 Go 值提供了反射接口。两个简单的函数, reflect.TypeOf 和 reflect.ValueOf ,返回被检查对象的类型和值。例如,x 被定义为: var xfloat64 = 3.4 ,那么 reflect.TypeOf(x) 返回 float64 , reflect.ValueOf(x) 返回 <float64 Value>。

3.反射可以从接口值反射到对象,也可以从对象反射回接口值。reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。一个重要的例子是 Value 有一个 Type 方法返回reflect.Value 的 Type。另一个是 Type 和 Value 都有 Kind 方法返回一个常量来表示类型:Uint、Float64、Slice 等等。同样 Value 有叫做 Int 和 Float 的方法可以获取存储在内部的值(跟 int64 和 float64 一样)。

4.Kind 总是返回底层类型:

type MyInt int
var m MyInt = 5
v := reflect.ValueOf(m)

方法 v.Kind() 返回 reflect.Int 。变量 v 的 Interface() 方法可以得到还原(接口)值,所以可以这样打印 v 的值: fmt.Println(v.Interface())。

package main
import ("fmt""reflect"
)
func main() {var x float64 = 3.4fmt.Println("type:", reflect.TypeOf(x)) //变量类型v := reflect.ValueOf(x)                      //变量值fmt.Println("value:", v)                fmt.Println("type:", v.Type())         //类型fmt.Println("kind:", v.Kind())         //类型fmt.Println("value:", v.Float())       //float值fmt.Println(v.Interface())                  //接口值fmt.Printf("value is %5.2e\n", v.Interface())y := v.Interface().(float64)               //float64打印接口值fmt.Println(y)
}

5.通过反射修改(设置)值:Value 有一些方法可以完成这个任务,但是必须小心使用: v.SetFloat(val) 。是否可设置是 Value 的一个属性,并且不是所有的反射值都有这个属性:可以使用 CanSet() 方法测试是否可设置。在例子中我们看到 v.CanSet() 返回 false: settability of v: false。 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x。要想 v 的更改能作用到 x,那就必须传递 x 的地址 v = reflect.ValueOf(&x) 。通过 Type() 我们看到 v 现在的类型是 *float64 并且仍然是不可设置的。要想让其可设置我们需要使用 Elem() 函数,这间接的使用指针: v = v.Elem()。

package main
import ("fmt""reflect"
)
func main() {var x float64 = 3.4v := reflect.ValueOf(x) //生成reflect对象// setting a value:// v.SetFloat(3.1415) // Error: will panic: reflect.Value.SetFloat using unaddressable valuefmt.Println("settability of v:", v.CanSet()) //检查属性是否可以更改v = reflect.ValueOf(&x) // Note: 必须传入指针地址,而不是拷贝fmt.Println("type of v:", v.Type()) //打印是否是指针类型fmt.Println("settability of v:", v.CanSet())v = v.Elem() //更改可以进行设置fmt.Println("The Elem of v is: ", v)fmt.Println("settability of v:", v.CanSet()) //检查更改是否已经完成v.SetFloat(3.1415) // 更改属性fmt.Println(v.Interface()) //检查接口值fmt.Println(v)
}

6.反射结构:反射一个结构类型。结构中只有被导出字段(首字母大写)才是可设置的, NumField() 方法返回结构内的字段数量;通过一个 for 循环用索引取得每个字段的值 Field(i) 。我们同样能够调用签名在结构上的方法,例如,使用索引 n 来调用: Method(n).Call(nil) 。

package main
import ("fmt""reflect"
)
type NotknownType struct { //结构s1, s2, s3 string
}
func (n NotknownType) String() string { //结构具有的方法return n.s1 + " - " + n.s2 + " - " + n.s3
}
// 空接口赋值
var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}
func main() {value := reflect.ValueOf(secret) // <main.NotknownType 值>typ := reflect.TypeOf(secret) // main.NotknownType 类型// alternative://typ := value.Type() // main.NotknownTypefmt.Println(typ)knd := value.Kind() // 接口值的种类fmt.Println(knd)// iterate through the fields of the struct:for i := 0; i < value.NumField(); i++ {  //遍历属性fmt.Printf("Field %d: %v\n", i, value.Field(i)) //具体属性// error: panic: reflect.Value.SetString using value obtained using unexported field//value.Field(i).SetString("C#")}// call the first method, which is String():results := value.Method(0).Call(nil) //调用第一个接口方法,传入0参fmt.Println(results) // [Ada - Go - Oberon]
}

结构中设置值示例:

package main
import (
"fmt"
"reflect"
)
type T struct {
A int
B string
}
func main() {
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t) //t is now {77 Sunset Strip}
}

十一、Printf 和反射

1.在Go 语言的标准库中,反射的功能被大量地使用。fmt 包中的 Printf(以及其他格式化输出函数)都会使用反射来分析它的 ... 参数。
Printf 的函数声明为:func Printf(format string, args ... interface{}) (n int, err error)。Printf 中的 ... 参数为空接口类型。Printf 使用反射包来解析这个参数列表。所以,Printf 能够知道它每个参数的类型。因此格式化字符串中只有%d而没有 %u 和 %ld,因为它知道这个参数是 unsigned 还是 long。这也是为什么 Print 和Println 在没有格式字符串的情况下还能如此漂亮地输出。

2.使用print函数,参数为空接口,不需要输入参数类型,示例。

package main
import ("os""strconv"
)
type Stringer interface {  //字符串转换接口String() string
}
type Celsius float64      //浮点数
func (c Celsius) String() string { //变量实现字符串转换接口
//将浮点数转换字符串,'f'(-ddd.dddd),prec控制精度(排除指数部分):对'f'表示小数点后的数字个数;bitSize表示f的来源类型(32:float32、64:float64return strconv.FormatFloat(float64(c),'f', 1, 64) + " °C"
}
type Day int
var dayName = []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
func (day Day) String() string {return dayName[day]
}
func print(args ...interface{}) { //打印函数,参数为。。。空接口for i, arg := range args {if i > 0 {os.Stdout.WriteString(" ")} switch a := arg.(type) { // type switch //针对不同种类采取不同方式打印case Stringer: os.Stdout.WriteString(a.String())case int: os.Stdout.WriteString(strconv.Itoa(a))case string: os.Stdout.WriteString(a)// more typesdefault: os.Stdout.WriteString("???")}}
}
func main() {print(Day(1), "was", Celsius(18.36)) // Tuesday was 18.4 °C
}

十二、接口与动态类型

1.Go 的动态类型:Go 是唯一结合了接口值,静态类型检查(是否该类型实现了某个接口),运行时动态转换的语言,并且不需要显式地声明类型是否满足某个接口。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。接收一个(或多个)接口类型作为参数的函数,其实参可以是任何实现了该接口的类型。 实现了某个接口的类型可以被传给任何以此接口为参数的函数, 没有实现接口方法不会调用该函数。

package main
import "fmt"
type IDuck interface {Quack()Walk()
}
func DuckDance(duck IDuck) {for i := 1; i <= 3; i++ {duck.Quack()duck.Walk()}
}
type Bird struct {// ...
}
func (b *Bird) Quack() {fmt.Println("I am quacking!")
}
func (b *Bird) Walk() {fmt.Println("I am walking!")
}
func main() {b := new(Bird)DuckDance(b)
}

2. 动态方法调用:Go 的实现通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 interface{} 这样的“泛型”上,你可以通过类型断言来检查变量是否实现了相应接口。Go 在这里用了和 gob 相同的机制:定义了两个接口 GobEncoder 和 GobDecoder 。这样就允许类型自己实现从流编解码的具体方式;如果没有实现就使用标准的反射方式。因此 Go 提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。你用不同的类型表示 XML 输出流中的不同实体。然后我们为 XML 定义一个如下的“写”接口(甚至可以把它定义为私有接口):

type xmlWriter interface {
WriteXML(w io.Writer) error
}

现在我们可以实现适用于该流类型的任何变量的 StreamXML 函数,并用类型断言检查传入的变量是否实现了该接口;如果没有,我们就调用内建的 encodeToXML 来完成相应工作:

// Exported XML streaming function.
func StreamXML(v interface{}, w io.Writer) error {
if xw, ok := v.(xmlWriter); ok {
// It’s an xmlWriter, use method of asserted type.
return xw.WriteXML(w)
}
// No implementation, so we have to use our own function (with perhaps reflection):
return encodeToXML(v, w)
}
// Internal XML encoding function.
func encodeToXML(v interface{}, w io.Writer) error {
// ...
}

3.接口的提取:提取接口 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维
护整个的类层次结构。Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行
为,这时就可以创建一个接口来使用。 假设我们需要一个新的接口TopologicalGenus ,用来给 shape 排序(这里简单地实现为返回 int)。我们需要做的是给想要满足接口的类型实现Rank() 方法。

//multi_interfaces_poly.go
package main
import "fmt"
type Shaper interface {Area() float32
}
type TopologicalGenus interface {Rank() int
}
type Square struct {side float32
}
func (sq *Square) Area() float32 {return sq.side * sq.side
}
func (sq *Square) Rank() int {return 1
}
type Rectangle struct {length, width float32
}
func (r Rectangle) Area() float32 {return r.length * r.width
}
func (r Rectangle) Rank() int {return 2
}
func main() {r := Rectangle{5, 3} // Area() of Rectangle needs a valueq := &Square{5} // Area() of Square needs a pointershapes := []Shaper{r, q}fmt.Println("Looping through shapes for area ...")for n, _ := range shapes {fmt.Println("Shape details: ", shapes[n])fmt.Println("Area of this shape is: ", shapes[n].Area())}topgen := []TopologicalGenus{r, q}fmt.Println("Looping through topgen for rank ...")for n, _ := range topgen {fmt.Println("Shape details: ", topgen[n])fmt.Println("Topological Genus of this shape is: ", topgen[n].Rank())}
}

4.显式地指明类型实现了某个接口:如果你希望满足某个接口的类型显式地声明它们实现了这个接口,你可以向接口的方法集中添加一个具有描述性名字的方法。例如:

type Fooer interface {
Foo()
ImplementsFooer()
}

类型 Bar 必须实现 ImplementsFooer 方法来满足 Fooer 接口,以清楚地记录这个事实。大部分代码并不使用这样的约束,因为它限制了接口的实用性。但是有些时候,这样的约束在大量相似的接口中被用来解决歧义:

type Bar struct{}
func (b Bar) ImplementsFooer() {}
func (b Bar) Foo() {}

5.空接口和函数重载: 我们看到函数重载是不被允许的。在 Go 语言中函数重载可以用可变参数 ...T 作为函数最后一个参数来实
现。如果我们把 T 换为空接口,那么可以知道任何类型的变量都是满足 T (空接口)类型的,这样就允许我们传递任何数量任何类型的参数给函数,即重载的实际含义。
函数 fmt.Printf 就是这样做的:
fmt.Printf(format string, a ...interface{}) (n int, errno error)
这个函数通过枚举 slice 类型的实参动态确定所有参数的类型。并查看每个类型是否实现了 String() 方法,如果是就用于产生输出信息。

6.接口的继承:当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的
接口方法。例如:

type Task struct {
Command string
*log.Logger
}

这个类型的工厂方法像这样:

func NewTask(command string, logger *log.Logger) *Task {
return &Task{command, logger}
}

当 log.Logger 实现了 Log() 方法后,Task 的实例 task 就可以调用该方法:task.Log()。类型可以通过继承多个接口来提供像 多重继承 一样的特性:

type ReaderWriter struct {
*io.Reader
*io.Writer
}

7.有用的接口可以在开发的过程中被归纳出来。添加新接口非常容易,因为已有的类型不用变动(仅仅需要实现新接口的方法)。已有的函数可以扩展为使用接口类型的约束性参数:通常只有函数签名需要改变。对比基于类的 OO 类型的语言在这种情况下则需要适应整个类层次结构的变化。

十三、总结

Go 没有类,而是松耦合的类型、方法对接口的实现。
OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢?
封装(数据隐藏):和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层:
1)包范围内的:通过标识符首字母小写, 对象 只在它所在的包内可见;
2)可导出的:通过标识符首字母大写, 对象 对所在包以外也可见。
类型只拥有自己所在包中定义的方法。
继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现;
多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。

十四、结构体、集合和高阶函数

有点复杂,通常你在应用中定义了一个结构体,那么你也可能需要这个结构体的(指针)对象集合,比如:

type Any interface{}
type Car struct {
Model string
Manufacturer string
BuildYear int
// ...
}
type Cars []*Car

在定义所需功能时我们可以利用函数可以作为(其它函数的)参数的事实来使用高阶函数,例如:
1)定义一个通用的 Process() 函数,它接收一个作用于每一辆 car 的 f 函数作参数:

// Process all cars with the given function f:
func (cs Cars) Process(f func(car *Car)) {
for _, c := range cs {
f(c)
}
}

2)在上面的基础上,实现一个查找函数来获取子集合,并在 Process() 中传入一个闭包执行(这样就可以访问局部切
片 cars ):

// Find all cars matching a given criteria.
func (cs Cars) FindAll(f func(car *Car) bool) Cars {
cars := make([]*Car, 0)
cs.Process(func(c *Car) {
if f(c) {
cars = append(cars, c)
}
})
return cars
}

3)实现 Map 功能,产出除 car 对象以外的东西:

// Process cars and create new data.
func (cs Cars) Map(f func(car *Car) Any) []Any {
result := make([]Any, 0)
ix := 0
cs.Process(func(c *Car) {
result[ix] = f(c)
ix++
})
return result
}

现在我们可以定义下面这样的具体查询:

allNewBMWs := allCars.FindAll(func(car *Car) bool {
return (car.Manufacturer == "BMW") && (car.BuildYear > 2010)
})

4)我们也可以根据入参返回不同的函数。也许我们想根据不同的厂商添加汽车到不同的集合,但是这可能会是多变
的。所以我们可以定义一个函数来产生特定的添加函数和 map 集:

func MakeSortedAppender(manufacturers[]string)(func(car*Car),map[string]Cars) {
// Prepare maps of sorted cars.
sortedCars := make(map[string]Cars)
for _, m := range manufacturers {
sortedCars[m] = make([]*Car, 0)
}
sortedCars["Default"] = make([]*Car, 0)
// Prepare appender function:
appender := func(c *Car) {
if _, ok := sortedCars[c.Manufacturer]; ok {
sortedCars[c.Manufacturer] = append(sortedCars[c.Manufacturer], c)
} else {
sortedCars["Default"] = append(sortedCars["Default"], c)
}
}
return appender, sortedCars
}

现在我们可以用它把汽车分类为独立的集合,像这样:

manufacturers := []string{"Ford", "Aston Martin", "Land Rover", "BMW", "Jaguar"}
sortedAppender, sortedCars := MakeSortedAppender(manufacturers)
allUnsortedCars.Process(sortedAppender)
BMWCount := len(sortedCars["BMW"])

全部代码注释:

// cars.go
package main
import ("fmt"
)
type Any interface{} //空接口
type Car struct {    //Car数据结构Model stringManufacturer stringBuildYear int// ...
}
type Cars []*Car //car结构指针对象空切片
func main() {// make some cars:初始化车辆,并取其地址ford := &Car{"Fiesta", "Ford", 2008}bmw := &Car{"XL 450", "BMW", 2011}merc := &Car{"D600", "Mercedes", 2009}bmw2 := &Car{"X 800", "BMW", 2008}// query:生成指针对象集合allCars := Cars([]*Car{ford, bmw, merc, bmw2})allNewBMWs := allCars.FindAll(func(car *Car) bool {return (car.Manufacturer == "BMW") && (car.BuildYear > 2010)})fmt.Println("AllCars: ", allCars) //打印所有车fmt.Println("New BMWs: ", allNewBMWs) //符合条件的车//manufacturers := []string{"Ford", "Aston Martin", "Land Rover", "BMW", "Jaguar"}sortedAppender, sortedCars := MakeSortedAppender(manufacturers)allCars.Process(sortedAppender) //遍历所有车fmt.Println("Map sortedCars: ", sortedCars)BMWCount := len(sortedCars["BMW"]) //打印宝马数量fmt.Println("We have ", BMWCount, " BMWs")
}
// Process all cars with the given function f: 根据给予的函数处理所有的车
func (cs Cars) Process(f func(car *Car)) {for _, c := range cs {f(c)}
}
// Find all cars matching a given criteria. //根据给与的标准(函数)匹配合适的车辆
func (cs Cars) FindAll(f func(car *Car) bool) Cars {cars := make([]*Car, 0)  //生成长度为0的[]*Car类型的切片cs.Process(func(c *Car) {if f(c) {            //符合传入函数条件cars = append(cars, c) //将符合条件车地址添加到cars切片}})return cars
}
// Process cars and create new data.
func (cs Cars) Map(f func(car *Car) Any) []Any {result := make([]Any, len(cs))ix := 0cs.Process(func(c *Car) {result[ix] = f(c)ix++})return result
}
func MakeSortedAppender(manufacturers []string) (func(car *Car), map[string]Cars) {// Prepare maps of sorted cars.sortedCars := make(map[string]Cars) //制造厂商和车辆映射数组for _, m := range manufacturers {sortedCars[m] = make([]*Car, 0) //每一个厂商赋值nil指针}sortedCars["Default"] = make([]*Car, 0) //default设置为nil// Prepare appender function:appender := func(c *Car) { //按照制造厂商将指针分类if _, ok := sortedCars[c.Manufacturer]; ok {sortedCars[c.Manufacturer] = append(sortedCars[c.Manufacturer], c)} else {sortedCars["Default"] = append(sortedCars["Default"], c)}}return appender, sortedCars
}

 

  相关解决方案