Go语言的接口与反射
美女图片没啥用,就是为了好看
本文还在完善中...
go总体而言是一门比较好入门的语言,许多特性都很精简易懂,但是接口与反射除外。他们真的让人头疼,不知道是自身资质问题还是怎么着,总是觉得很多书上写的不够精简明了。。而我,亚楠老猎人,今天就是要受苦试着把它给攻克了。
接口
你可以用很多词语来形容golang,但“传统”肯定不能用。因为,它里面没有类和继承的概念。
你觉得这简直不可思议,怎么可能这样,那不是意味着海量的重复代码。并没有,Go通过很灵活的一个概念,实现了很多面向对象的行为。没错,这个概念就是“接口”。
Go 没有类:数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。
我们来看看接口的特性。
接口被隐式实现
类型不需要显式声明它实现了某个接口,接口是被隐式地实现的。
什么意思?就是说只要你把接口声明的方法都实现了,那么就认为你实现了这个接口了。无需像其他语言那样在显眼的地方表明 implements 接口名称
,比如php中你可能需要这样子:
<?php
interface Cinema
{
public function show(Order $show,$num);
}
// 显示正常
class Order implements Cinema
{
public $number='0011排';
public function show(Order $show,$num)
{
echo $show->number.$num;
}
}
$face= new Order();
$face->show(new Order,$num='3人');//输出 0011排3人
而在golang中,你只需要这个样子:
// 一个简单的求正方形面积的例子
package main
import "fmt"
// 形状接口
type Shape interface {
Area() float32
}
// 输出形状面积
func PrintArea(shape Shape) {
fmt.Printf("The square has area: %f\n", shape.Area())
}
// 正方形结构体
type Square struct {
side float32
}
// 正方形面积
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
func main() {
square := new(Square)
square.side = 5
PrintArea(square)
}
上面的程序定义了一个结构体 Square 和一个接口 Shape,接口有一个方法 Area(),而Square实现了这个方法,虽然没有显示声明。
这时你发现,PrintArea
这个函数居然可以直接接受了Square类型的参数,尽管函数定义里,参数是Shape接口类型的。
也就是说,golang认为你已经用Square结构体实现了Shape接口。
如果,我们对代码稍作修改,给接口定义中增加周长(Perimeter)方法
// 形状接口
type Shape interface {
Area() float32
Perimeter() float32
}
其他不作改动,你就会发现编译器报错了
cannot use square (type *Square) as type Shape in argument to DescArea:
*Square does not implement Shape (missing Perimeter method)
报错信息说的很明了,Shape还有个方法Perimeter,但是Square却未实现它。虽然还没有人去调用这个方法,但是编译器也会提前给出错误。
下面我们准备开始了解继承与多态,在开始之前,我们记住这句话
一个接口可以由多种类型实现,一种类型也可以实现多个接口。
接口实现继承
一个接口可以包含一个或者多个其他的接口,这相当于直接把这些内嵌接口的方法列举在外层接口中一样。
当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)的指针时,这个类型就可以使用(另一个类型)所有的接口方法。
比如,还是那个Shape的例子,我们这次增加一个要素,颜色,来生成多彩的正方形。
package main
import "fmt"
// 形状接口
type Shape interface {
Area() float32
}
// 颜色接口
type Color interface {
Colors() []string
}
// 多彩的形状接口
type ColorfulShape interface {
Shape
Color
Name()
}
比如上面的例子,最后的ColorfulShape
就包含了Shape和Color接口,此外还有自身特有的Name()方法。
接口实现多态
我们很容易扩展之前的代码,比如你可以联想到正方形的好兄弟,长方形,于是..
package main
import "fmt"
// 形状接口
type Shape interface {
Area() float32
}
// 输出形状面积
func PrintArea(shape Shape) {
fmt.Printf("The square has area: %f\n", shape.Area())
}
// 正方形结构体
type Square struct {
side float32
}
// 正方形面积
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
// 长方形结构体
type Rectangle struct {
length, width float32
}
// 长方形面积
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func main() {
r := Rectangle{5, 3}
q := &Square{5}
shapes := []Shape{r, q}
fmt.Println("Looping through shapes for area ...")
for key, _ := range shapes {
fmt.Println("Shape details: ", shapes[key])
fmt.Println("Area of this shape is: ", shapes[key].Area())
}
}
在main方法的for循环中,虽然只知道shapes[key]是一个Shape对象,但是它却能自动变成Square或者Rectangle对象,还可以调用各自的Area方法。是不是很厉害?
通过上面的例子,我们可以发现:
- 接口其实像一种契约,实现类型必须满足它(实现其定义的方法)。
- 接口描述了类型的行为,规定类型可以做什么。
- 接口彻底将类型能做什么,以及如何做分离开来。
- 这些特点使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
使用接口使代码更具有普适性。
总结一下golang里面的继承与多态:
- 继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现
- 多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且:接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
类型断言
前面用接口实现多态时,在最后main方法的for循环里,接口类型变量
shapes[key]中可以包含任何类型的值,那么如何检测当前的对象是什么类型的呢?
答案就是使用类型断言。比如
v := var.(类型名)
这里的var必需得是接口变量,比如shapes[key]。
如果我们直接这么写
v := shapes[key].(*Square)
那肯是会报错的,因为shapes[key]也可能是Rectangle类型的,为了避免错误发生,我们可以使用更安全的方法进行断言:
if v, ok := shapes[key].(*Square); ok {
// 相关操作
}
如果转换合法,v 是 shapes[key] 转换到类型 Square 的值,ok 会是 true;否则 v 是类型 Square 的零值,ok 是 false,也没有运行时错误发生。
备注: 不要忽略
shapes[key].(*Square)
中的*
号,否则会导致编译错误:impossible type assertion: Square does not implement Shape (Area method has pointer receiver)。
方法集与接口
Go 语言规范定义了接口方法集的调用规则:
- 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
- 类型 T 的可调用方法集包含接受者为 T 的所有方法
- 类型 T 的可调用方法集不包含接受者为 *T 的方法
举例说明
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 value
var 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 type
fmt.Printf("- lst is long enough\n")
}
// A pointer value
plst := new(List)
CountInto(plst, 1, 10) //VALID:Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}
在 lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的,因为 Len 定义在值上。
在 plst 上调用 CountInto 是可以的,因为 CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。
空接口
空接口是个很厉害的存在,因为他什么方法都没有。那么,你就可以认为所有的结构体都实现了这个接口。哪怕这个结构体什么事情都不做。这大概就是所谓的“无招胜有招吧”。
反射
Reflection(反射)在计算机中表示程序能够检查自身结构的能力,尤其是类型。它是元编程的一种形式,也是最容易让人迷惑的一部分。
这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。
变量最基本的信息其实就是:类型
和值
。而反射包中的reflect.Type
还有reflect.Value
就是用来返回对象的类型和值。比如:
var x float64 = 3.14
fmt.Println(reflect.TypeOf(x)) // float64
fmt.Println(reflect.ValueOf(x)) // 3.14
反射与接口的联系
那么反射和接口到底有什么关系呢?
观察上面两个函数的定义就可以明显看出来了,比如TypeOf
:
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
反射方法首先把变量转化为空接口(按照上文所述,所有结构体都可以转化为空接口)。
反射的一些方法
对于 float64 类型的变量 x,如果 v := reflect.ValueOf(x),那么 v.Kind() 返回 reflect.Float64 ,所以下面的表达式是 true
v.Kind() == reflect.Float64
通过反射修改(设置)值
如果我们想要修改x的值怎么办,最简单的方法当然是
x = 3.1415
还有一种方法就是使用SetFloat
方法,但是如果你这么使用
v.SetFloat(3.1415)
却会报错reflect.Value.SetFloat using unaddressable value
。这是为什么呢?因为v不是可设置的(这里并不是说值不可寻址)。是否可设置是 Value 的一个属性,并且不是所有的反设值都有这个属性:可以使用 CanSet() 方法测试是否可设置。
当 v := reflect.ValueOf(x) 函数通过传递一个 x 拷贝创建了 v,那么 v 的改变并不能更改原始的 x。要想 v 的更改能作用到 x,那就必须传递 x 的地址 v = reflect.ValueOf(&x)。
但是通过 Type() 我们看到 v 现在的类型是 *float64 并且仍然是不可设置的。
要想让其可设置我们需要使用 Elem() 函数,这间接的使用指针:
v = v.Elem()
现在 v.CanSet() 返回 true 并且 v.SetFloat(3.1415) 设置成功了!代码如下:
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
v := reflect.ValueOf(x)
// setting a value:
// v.SetFloat(3.1415) // Error: will panic: reflect.Value.SetFloat using unaddressable value
fmt.Println("settability of v:", v.CanSet())
v = reflect.ValueOf(&x) // Note: take the address of x.
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) // this works!
fmt.Println(v.Interface())
fmt.Println(v)
}
反射结构
有些时候需要反射一个结构类型。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
}
// variable to investigate:
var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}
func main() {
value := reflect.ValueOf(secret) // <main.NotknownType Value>
typ := reflect.TypeOf(secret) // main.NotknownType
// alternative:
//typ := value.Type() // main.NotknownType
fmt.Println(typ)
knd := value.Kind() // struct
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)
fmt.Println(results) // [Ada - Go - Oberon]
}
标准库中运用的反射
Go语言的标准库中其实也大量运用了反射,比如说fmt
包中的Printf
,就使用了反射来分析它的...
参数。
看看Printf函数的定义
// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...interface{}) string {
p := newPrinter()
p.doPrintf(format, a)
s := string(p.buf)
p.free()
return s
}
其中的...interface{}
就是空接口类型。该函数使用反射,从而得知每个参数的类型。所以,你无需在格式化字符串中使用%u
或者%ld
来指明参数是unsigned
或是long
,只需要%d
就好了,因为它知道这个参数是什么类型的。
下面是一个简化版的Printf
函数,来简要说明其原理
package main
import (
"os"
"strconv"
)
type Stringer interface {
String() string
}
type Celsius float64
func (c Celsius) String() string {
return 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 types
default: os.Stdout.WriteString("???")
}
}
}
func main() {
print(Day(1), "was", Celsius(18.36)) // Tuesday was 18.4 °C
}
接口的提取
提取接口 是非常有用的设计模式,可以减少需要的类型和方法数量,而且不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
Go 接口可以让开发者找出自己写的程序中的类型。假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。
所以你不用提前设计出所有的接口;整个设计可以持续演进,而不用废弃之前的决定。类型要实现某个接口,它本身不用改变,你只需要在这个类型上实现新的方法。