4. 黑科技 Interface |《 刻意学习 Golang 》

看完 Go 的 Interface 后,整体感觉这不是我认识的 Interface,有很多新东西跟黑科技。下面通过对比 PHP以及 demo 来进行理解。 PHP 有构造函数、很多魔术方法,为什么 Golang 没有?或者说不需要呢 PHP 对对象初始化赋值 $a = new Human("Jerr", 23); 需要通过构造函数实现 Go jerr := Human{"Jerr", 23} ,这里是很优秀。 但是 PHP 的构造函数还可以做一些前置处理,目前还不知道 Go 在这一块怎么解决,可能通过包的 init() ,且学且看

最后发现 Golang 官方的关于反射讲解的,需要翻墙同时是外文,准备明天「周六」给翻译过来。

译文:(译)反射的法则 | 《 The Go Blog 》

Interface 是什么

PHP中:

可以指定某个类必须实现哪些方法,但不需要定义这些方法的具体内容

Interface 定义一个接口类「只有定义,没有实现」,通过 implements 来实现一个或多个接口类中定义的方法「必须实现接口类中的所有方法」

// 定义 Human 接口有 run 跟 say 两个方法
interface HumanInterface {
    public function run();
    public function say();
}
// 实现 Human 接口 必须实现所有方法 run 与 say
class Human implements HumanInterface {
    public $age;
    public $name;
    public function run() {
        echo 'I can run';
    }
    public function say() {
        echo 'I can say';
    }
}
// 歌手
class Singer extends Human {
    public $collection; // 专辑
    public function sing() {
        echo 'I can sing'
    }
}
// 学生
class Student extends Human {
    public $lesson;
    public function learn() {
        echo "I need learn {$lesson}";
    }
}

Golang 中:

interface 是一组 method 签名的组合,我们通过 interface 来定义对象的一组行为。

简单说 interface 类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口

// 这里我们用 go 来做上面 php 类似的事情
// 定义 interface
type HumanInterface interface {
    Say()
    Run()
}
// 定义 Human 类「结构体」
type Human struct {
    name string
    age int
}
// 实现 Human 的 Say() 方法
func (h Human) Say() {
    fmt.Printf("I can say, I am %s, %d years old\n", h.name, h.age)
}
// 实现 Human 的 Run() 方法
func (h Human) Run() {
    fmt.Printf("%s is running\n", h.name)
}
// 歌手
type Singer struct {
    Human
    collecton string
}
// 歌手的 Sing 方法
func (s Singer) Sing() {
    fmt.Printf("%s can sing %s\n", s.name, s.collecton)
}

func main() {
    tom := Singer{Human{"Tom", 22}, "《Tom专辑歌曲》"}
    tom.Run()
    tom.Say()
    tom.Sing()
}

你有没有发现 PHP 的 class 通过 implement 来实现 interface 的所有方法。

而这里 Golang 的 interface 似乎没啥用,甚至一点都没用上,而且你在 Human 里面只实现 run 或者 say 程序都正常运行。如果你对 golang 的 interface 有疑惑,且继续往下看

要揭开 Golang interface 神秘的面纱,先来了解下 interface 类型跟 interface 值

interface 的 类型 与 值

interface 类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

换句话说:如果某个对象实现了某个接口的所有方法,则这些对象属于这个接口「你可理解为对类做了一个分类」

  • PHP:接口定义了一系列方法,类显式的去 implement 这个接口并必须实现接口的所有方法
  • Golang:接口定义了一组方法,类定义了一组方法,如果类里有接口的所有方法「即类实现了接口」

更直观的理解:控制器在类手里,我类实现了接口所有的方法我属于你,我也可以不实现所有方法,我就不属于你

我们再来看 interface 的值,这个更好玩

如果我们定义了一个 interface 的变量,那么这个变量里面可以存实现这个 interface 的任意类型的对象

直接看代码

// 控制权在类手里,所以我们先定义类跟类的方法吧 下面定义的代码基本跟上面的一样 你可以直接看 main 里面的代码
type Human struct {
    name string
    age int
}

func (h Human) Say() {
    fmt.Printf("I can say, I am %s, %d years old\n", h.name, h.age)
}

func (h Human) Run() {
    fmt.Printf("%s is running\n", h.name)
}

type Singer struct {
    Human
    collecton string
}

func (s Singer) Sing() {
    fmt.Printf("%s can sing %s\n", s.name, s.collecton)
}

type Student struct{
    Human
    lesson string
}

func (s Student) Learn() {
    fmt.Printf("%s nee learn %s\n", s.name, s.lesson)
}

// interface
type Men interface {
    Say()
    Run()
}


func main() {
    tom := Singer{Human{"Tom", 22}, "《Tom专辑歌曲》"}
    peter := Student{Human{"Perter", 18}, "《算法与数据结构 C 语言描述》"}
    jerrk := Human{"jerrk", 26}

    var men Men
    // men 存 tom
    men = tom
    men.Say()
    men.Run()
    // men.Sing() 不能调用 Sing() Men 没有 Sing()
    // men 存 peter
    men = peter
    men.Say()
    men.Run()
    // men.Learn() 同理
    // men 存 jerrk
    men = jerrk
    men.Say()
    men.Run()

    x := make([]Men, 3)
    // 这三个都是不同类型的元素,但是他们实现了 interface 同一个接口
    x[0], x[1], x[2] = tom, peter, jerrk

    for _, value := range x{
        value.Say()
        value.Run()
    }
}

Go 通过 interface 实现了 duck-typing: 即 “当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”

空 interface

空 interface 就是没有任何 method,顺理成章的所有的类型都实现了空 interface,所以空 interface 可以存储任意类型的值类似于 C 的 void * PHP 的 $a

作为函数参数

interface 变量持有任意实现该 interface 类型的对象,所以我们可以通过它对我们的函数参数类型做任何我们想要的扩展 比如 fmt.Println 可以接收任意类型的数据,

// 源码中 interface 参数的定义
type Stringer interface {
     String() string
}

默认有个 String 方法的实现,fmt.Println(a) 会调用 a.String() 输入 a, 你可以自己重写 String 来改变默认输出

变量存储的类型

interface 的变量刻意存储任意实现了该接口的类的实例,我们通过 Comma-ok 断言 switch 测试 反过来知道这个变量里面实际保存了的是哪个类型的对象

  • Comman-ok
  • switch 测试

直接看代码

// value, ok = element.(T)
// value 就是变量的值,ok 是一个 bool 类型,element 是 interface 变量,T 是断言的类型
// 空 interface
type Element interface {}
type List []Element

type Person struct {
    name string
    age int
}

func main() {
    list := make(List, 3)
    list[0] = 1 // int
    list[1] = "hello"   // string
    list[2] = Person{"Jack", 29}
    fmt.Println(list)
    for index, element := range list {
        if value, ok := element.(int); ok {
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("list[%d] is an string and its value is %s\n", index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("list[%d] is an Person and its value is %s\n", index, value)
        } else {
            fmt.Println("Unknow type")
        }
    }
    
    // switch
    for index, element := range list{
        switch value := element.(type) { // element.(type) 只能在 switch 内部使用 外部使用 comma-ok
            case int:
                fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
            case string:
                fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
            case Person:
                fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
            default:
                fmt.Println("list[%d] is of a different type", index)
        }
    }
}

element.(type) 只能在 switch 内部使用 外部使用 comma-ok

嵌入 interface

Golang 的 interface 跟 struct 的匿名字段一样,可以嵌入到新的 interface 里面「隐式的包含了 被嵌者 里面的 method」

io 包下面的 io.ReadWriter ,它包含了 io 包下面的 Reader 和 Writer 两个 interface:

// io.ReadWriter
type ReadWriter interface {
    Reader
    Writer
}

反射

PHP 中也实现了反射,我们先来看一下 PHP 中的反射

PHP 提供了一套强大的反射API,允许你在PHP运行环境中,访问和使用类、方法、属性、参数和注释等,其功能十分强大,经常用于高扩展的PHP框架,自动加载插件,自动生成文档,甚至可以用来扩展PHP语言

反射能做什么?换句话说一个类的反射类能做什么?

  • 获取类的所有属性
  • 获取类的所有方法
  • 获取类的所有常量
  • 获取类、方法、变量的文档注释
  • 创建类的示例
  • 调用指定实例的方法、设置变量
  • 修改对象的可访问性

详细可以去看官方文档 PHP 反射

步骤大概可以为

  • 构建类的反射类
  • 获取反射类能反射的数据
  • 通过反射 api 操作「创建实例、调用方法、修改变量…」

看如下代码

use ReflectionClass;
/**
 * @author Jerrk
 */
class Human {
    /*
     * @var string $name
     */
    public $name;
    public $age;
    public __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
    public function Say() {
        echo "{$this->name} can say";
    }
}
// 构建反射类
$class = new ReflectionClass('\Human')
$properties = $class->getProperties(); //获取所有属性
$property = $class->getProperty('name'); // 获取单一属性
$instance = $class->newInstance('Jerrk', 22);
$class->getProperty('name')->setValue($instance, 'Jerr');

Golang

这里需要注意的是 反射只能访问 public 变量不能访问 private,所以要想让反射刻意访问必须大写结构体的变量名

// 反射
i  := Human{"Jerrk", 22}
//t := reflect.TypeOf(&i)    // 得到类型的元数据,通过t我们能获取类型定义里面的所有元素
s := reflect.ValueOf(&i).Elem()   // 得到实际的值,还可以去改变值
typeOfi := s.Type(); // = reflect.TypeOf(&i) 获取类型

f2Name := typeOfi.Field(1).Name // 获取 第二个字段名 Age
f2Type := s.Field(1).Type() // 获取第二给字段值的类型 int
f2Value := s.Field(1).Interface() // 获取第二给字段的值 22

for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i, typeOfi.Field(i).Name, f.Type(), f.Interface())
}
// 改变值
s.Field(0).SetString("Peter") // 现在 Name 变成了 Peter
s.Field(1).SetInt(44) // 现在 Age 变成了 44

对比下来跟 PHP 还是有一些不一样的地方,但是符合反射的定义

reflection is just a mechanism to examine the type and value pair stored inside an interface variable

反射是一种检查程序运行时接口变量中的类型和值的机制

更多的反射相关的知识可以去看官方的 laws of reflection

同时也可以稍微等一天,我明天把 laws of reflection 翻译过来,深入理解 Golang 的 interface 跟 反射

译文:(译)反射的法则 | 《 The Go Blog 》