从Java到Golang-对interface的理解
If it walks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
如果一个东西走路、游泳、叫声都像鸭子,那么它大概就是一只鸭子。
对interface的理解
interface的实现
在JAVA里,interface定义了一个类的行为标准,里面可以包含不可变换的实体,例如没有函数体的函数或者不可改变的值比如final变量。一旦一个类implement这个interface,那么就要将这个interface的全部方法都实现。这样在工程上的好处是,解决了多继承问题(JAVA不能实现多继承,本质上是因为多个class之间的可变实体如同名变量,同名方法不同方法体会冲突),更高程度上抽象了一个类,只要知道这个类实现了什么接口,就可以调用接口的方法。在这个过程中不需要知道类中的具体实现,把函数注释写在接口的文件即可,保护了类。
在golang里,struct(类)定义了一系列的属性,并没有定义方法。如果这个struct想要有方法,就需要以func (cat Cat) func(input Type) output Type{ body }的形式给出,最开头的(cat Cat)即代表是这个CatStruct(类)的方法,它被称为receiver。这时一个struct如果实现了这个接口里定义的所有方法,可以称为实现了这个接口,就如一个东西做了很多鸭子做的事情那么它就是一只鸭子。注意这里面没有显式的说明某某struct实现某interface,只要全实现了,才能说明实现了这个interface。比如:
package main
import (
"fmt"
)
type Animal interface {
eat()
}
type Cat struct {
name string
}
// 这里Cat实现了Animal接口的全部方法,即Cat实现了Animal接口
func (cat Cat) eat() {
fmt.Println(cat.name + "eat")
}
func main() {
// 常规调用
var cat Cat
cat = Cat{name: "wu"}
cat.eat()
fmt.Println(cat.name)//还可以直接调用属性
// 赋值给接口变量 再调用方法
// 可以调用方法 不能调用属性 animal.name是错的
var animal Animal
animal = Cat{name: "mao"}
animal.eat()
}
而在JAVA里,类似的实现是这样的:
public class TestC {
public static void main(String[] args) {
// 调用类方法
new A().method();
}
}
interface In {
public void method();
}
// 使用implements显式声明实现接口In
class A implements In {
@Override
public void method() {
System.out.println("it's A");
}
}
如果把struct类比成类的话,他们的重载可以比作下图。实际上Go也可以实现像java那一对多。

利用interface做到多态
函数名前加上receiver即称为方法。不同receiver接收不同类型,甚至可以是基本类型如int(需要type定义,不能直接使用)。利用这个办法可以实现重载(一个方法对不同输入做出不同反应)。
package main
type Int int
type MyStruct struct {
name string
}
type MyStruct2 struct {
age string
}
func (a Int) process() int {
return int(a)
}
func (s MyStruct) process() string {
return s.name
}
func (s MyStruct2) process() string {
return s.age
}
func main() {
var a Int = 1
b := MyStruct{"ok"}
c := MyStruct2{"11"}
a.process()
b.process()
c.process()
}
可以看到,这样做并没有什么意义。因为最终还是要一个一个调用,没法像Java那样使用一个符号处理全部。能不能像Java那样在处理时,只使用一个符号呢。是可以实现的,利用空接口和类型断言。详见下一节。
interface作为函数参数
interface可以作为函数参数,代表某一类含有相同方法的struct(这些struct的对应的方法的交集为interface里的全部的方法)。传入的参数(interface类型)都有相同方法,即使每次传入参数名字不同,但是仍然可以书写一样的方法名进行调用;如果传入interface列表,也可以遍历列表调用。这个实现过程在JAVA里几乎是完全一致的。
package main
import "fmt"
type Animal interface {
speak()
}
type Cat struct {
voice string
}
type Dog struct {
voice string
}
// 实现接口Animal
func (cat Cat) speak() {
fmt.Println("cat can speak " + cat.voice)
}
// 实现接口Animal
func (dog Dog) speak() {
fmt.Println("dog can speak " + dog.voice)
}
// 传入接口Animal类型参数
func animalSpeak(animal Animal){
animal.speak()
}
// 体现了使用接口做参数的优越性,传入接口数组,批量调用方法
func animalAllSpeak(animals [2]Animal) {
for _, item := range animals {
item.speak()
}
}
func main() {
cat := Cat{voice: "miao"}
dog := Dog{voice: "wang"}
animalSpeak(cat)
animalSpeak(dog)
// 接口数组
list := [2]Animal{cat, dog}
animalAllSpeak(list)
}
在JAVA里类似的实现是这样的:
public class TestC {
public static void main(String[] args) {
A a = new A();
B b = new B();
// a是类A的实例,因为类A实现了In接口,所以可以作为In接口类型参数传入函数
new TestC().call(a);
new TestC().call(b);
// 通过数组传入,批量调用
In[] list = {a, b};
new TestC().callAll(list);
}
// 以In接口类型为函数参数
public void call(In imIn) {
imIn.method();
}
// 以In[]接口数组类型作为函数参数
public void callAll(In[] imIns) {
// foreach循环 for(元素类型 元素变量 : 遍历对象){使用元素变量}
for (In imIn : imIns) {
imIn.method();
}
}
}
interface In {
public void method();
}
// 类实现接口
class A implements In {
@Override
public void method() {
System.out.println("it's A");
}
}
// 类实现接口
class B implements In {
@Override
public void method() {
System.out.println("it's B");
}
}
当interface做参数的时候,若有struct传入(该struct实现了该interface),则传入的量保存了该struct的内部信息和struct种类,但是只能直接访问方法,不能直接访问内部变量。若要访问其struct的变量,要先进行类型断言i.(T)让编译器知道作为Interface传入的struct到底属于哪个struct,因为接口只是规定了实现方法没有规定里面能包含什么变量。
类型断言的语法格式如下:
value, ok := x.(T)
其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。
该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:
- 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
- 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
例子 :
package main
import "fmt"
type I interface {
Me()
}
type S struct {
a string
}
// Me 实现了接口,才能让struct以该接口类型传入函数
func (s S) Me() {
}
func main() {
var i I
i = S{"string inside"}
describe(i)
}
func describe(i I) {
//i是实现了接口的类型,但是作为接口传入就是接口,需要断言才能调用内部的值
// 可以直接访问方法
i.Me()
// b:=i.a 错误,不能直接访问,要加上类型断言,相当于强制转换
t, _ := i.(S)
b:=t.a
fmt.Println(b)
}
// 输出
// string inside
只要实现了interface的结构都可以作为此interface类型传入函数参数,那么空接口interface{}呢?要知道所有struct都包含空方法,所有struct都实现了空接口。因此,空interface可以作为通用类型代表任何struct。利用这个通用变量加上类型断言可以实现多态Reload。如下
package main
import "fmt"
type I interface {
Show()
}
type Dog struct {
name string
}
type Cat struct {
eat string
}
func (dog Dog) Show() {
fmt.Println(dog.name)
}
func (cat Cat) Show() {
fmt.Println(cat.eat)
}
func main() {
dog := Dog{"kiki"}
cat := Cat{"fish"}
ShowAll(dog)
ShowAll(cat)
}
func ShowAll(i I) {
// 直接访问方法不需要断言
i.Show()
t, ok := i.(Dog)
if ok {
fmt.Println("dog name " + t.name)
}
// 此处不能再用t保存实际值,要用新的变量r
r, ok := i.(Cat)
if ok {
fmt.Println("cat eat " + r.eat)
}
}
// 输出
// kiki
// dog name kiki
// fish
// cat eat fish
在JAVA里是:定义不同输入类型的同名方法(多个)–>实现重载。在Go里是:定义一个接收空接口interface{}类型的方法(可以只有一个)–>在方法内判断传入参数类型做出不同反应–>实现重载(这里的参数类型需要自己封装成一个struct,否则无法传入)。这里可以看出Go和Java的不同,Java允许你创建多个同名方法然后运行时帮你判断参数类型,然后自动调用你创建的匹配该参数类型方法;而Go更加底层一点,需要你自己实现判断参数类型的过程然后做出对应处理。
其实他们本质都是一样的,这个语言特性被称作反射(Reflection)。反射指的是,程序在运行过程中能够获得自身信息,例如函数main(param)运行时,他会知道自己叫"main",如果内部有定义变量,他可以知道变量名字,还可以在传入参数param时知道它的数据类型。注意这里的param可以在编写程序时不指定类型,只需要在调用这个函数时,利用语言标准库提供的一些方法就可以获得变量类型。反射的例子:
- 在Java里,
public Method[] getDeclaredMethods()可以获得当前类中的所有方法 - 在Java里,
public native boolean isInstance(Object obj)可以判断当前示例是否和传入对象的类型相同 - 在Go里,
func TypeOf(i interface{}) Type)可以得到传入参数i的类型 - 在Go里,
func (k Kind) String() string可以得到输入参数k的变量名字
反射这个特性赋予了语言“动态”的特性,静态语言编译运行,一切变量信息都在编译时定下,程序不会在运行时再获取自身信息。而反射允许语言在运行时动态获取信息。