Go 接口

Dec 26, 2018 22:55 · 1459 words · 3 minute read Golang

Go 语言内置了以下这些基础类型:

  • 布尔型:bool
  • 整型:int、byte、uint 等
  • 浮点型:float32、float64
  • 字符串:string
  • 字符:rune
  • 错误:error

这些都是具体的类型,Go 语言中还存在一种抽象类型,也就是接口类型,它不会暴露它所代表的对象的内部值的结构,而是展示出它们自己方法。简单来说,接口是一组方法的集合

Go 语言的接口引入了“非侵入式”概念,不需要使用“显式”的首发去实现。而 Java 和 C++ 通常会这样来“显示”地创建一个类去实现接口:

// Java
interface Animal {
    public void eat();
    public void sleep();
}

public class Mammal implements Animal {
    @Override
    public void eat() {
        System.out.println("Mammal eats.");
    }

    @Override
    public void sleep() {
        System.out.println("Mammal sleeps.");
    }
}
// C++
class Animal {
    public:
        virtual void eat() = 0;
        virtual void sleep() = 0;
}

class Mammal: public Animal {
    public:
        void eat() {
            cout << "Mammal eats." << endl;
        }

        void sleep() {
            cout << "Mammal sleeps." << endl;
        }
}

在实现接口前必须要先定义接口,并且将类型和接口紧密绑定(明确声明实现类继承了某个接口),这种就是“侵入式”接口。修改接口会影响到所有实现了该接口的类型。

而在 Go 语言中:

type Animal interface {
    eat()
    sleep()
}
type Mammal struct {}

func (m *Mammal) eat() {
    fmt.Println("Mammal eats.")
}

func (m *Mammal) sleep() {
    fmt.Println("Mammal sleeps.")
}

以上第二段代码本身也是完整可用的,和第一段没有直接关系。但是有了上面的接口类型定义后,Mammal 结构确实也拥有了 Animal 接口定义的所有方法,那么 Mammal 类型也就自动实现了 Animal 接口。

所以接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。

func main() {
    var animal Animal = new(Mammal)
    animal.eat()
    animal.sleep()
}

接口和类型可以直接转换,这种松散的对应关系系可以大幅降低因为接 口调整而导致的大量代码调整工作

接口值

接口赋值在 Go 语言中分为两种:

  1. 将对象实例赋值给接口
  2. 将一个接口赋值给另一个接口

将某种类型的对象实例赋值给接口,要求该对象实例实现了接口要求的所有方法。上面的代码对应的是第一种情况,Mammal 结构体类型实现了 eat()sleep() 两个 Animal 接口要求的方法。

定义两个接口,一个叫 Animal,一个叫 Mammal,两个接口都定义了 eat()sleep() 方法。

type Animal interface {
    eat()
    sleep()
}

type Mammal interface {
    sleep()
    eat()
}

type Human struct {}

func (h *Human) eat() {
    fmt.Println("Human eats.")
}

func (h *Human) sleep() {
    fmt.Println("Human sleeps.")
}

func main() {
    var human1 *Human = new(human)
    var human2 *Mammal = human1
    var human3 *Animal = human2
    ...
}

两个接口的拥有的方法完全相同(次序不同也没关系),那两者就是相同的,可以相互赋值。

空接口 interface

空接口不包含任何方法,对实现不做任何要求,类似 Java/C# 中所有类的基类:

type Any interface{}

所以空接口可以赋值任何类型的实例

最常用到的 fmt 标准库中的 Print() 系列方法,可以接受任意类型的参数:

func Printf(format string, a ...interface{}) (n int, err error) {
    return Fprintf(os.Stdout, format, a...)
}

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintln(a)
    n, err = w.Write(p.buf)
    p.free()
    return
}

类型断言

类型断言检查它操作对象的动态类型是否和断言的类型匹配

t := i.(T)

检查 i 的类型是否为 T,如果是的话返回 i 的动态值,如果不是,类型断言失败产生运行时错误。

确保被断言的接口值不是 nil。

type Animal interface {
    eat()
    sleep()
}

type Mammal struct {}

func (m *Mammal) eat() {
    fmt.Println("Mammal eats.")
}

func (m *Mammal) sleep() {
    fmt.Println("Mammal sleeps.")
}

func main() {
    var animal Animal = new(Mammal)
    mammal, ok := animal.(*Mammal)
    if ok {
        mammal.eat()
        mammal.sleep()
    }
}

error 接口

error 类型实质上就是一个接口:

type error interface {
    Error() string
}

对照 error 接口实现一个自定义的错误类型:

type CustomError struct {
    Op string
    Err error
}

func (customErr *CustomError) Error() string {
    return fmt.Sprintf("op: %s, err: %s\n", customErr.Op, customErr.Err.Error())
}

错误处理:

err := op() // 某个操作
if err != nil {
    if customErr, ok := err.(*CustomError); ok && e.Err != nil {
        // 错误处理
    }
}

类型开关

func newPrint(x interface{}) string {
	switch x := x.(type) {
	case nil:
		return "NULL"
	case int:
		return fmt.Sprintf("%d is int", x)
	case string:
		return fmt.Sprintf("%s is string", x)
	case bool:
		return fmt.Sprintf("%v is bool", x)
	case uint:
		return fmt.Sprintf("%d is uint", x)
	default:
		return fmt.Sprintf("unknown type!")
	}
}

func main() {
	fmt.Println(newPrint(1))
	fmt.Println(newPrint("hello"))
	fmt.Println(newPrint(true))
	fmt.Println(newPrint(1.0))
}

一个类型开关使用 switch 选择语句判断 x.(type),每个 case 有至少一种类型。每一个 case 会被顺序的进行考虑,如果匹配就会执行 case 中的语句。

建议

因为在 Go 语言中只有当两个或更多的类型实现一个接口时才使用接口,它们必定会从任意特 定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口。当新的类型出现时,小的接口更容易满足。对于接口设计的一 个好的标准就是 ask only for what you need(只考虑你需要的东西)。