Go 接口
Dec 26, 2018 22:55 · 1459 words · 3 minute read
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 语言中分为两种:
- 将对象实例赋值给接口
- 将一个接口赋值给另一个接口
将某种类型的对象实例赋值给接口,要求该对象实例实现了接口要求的所有方法。上面的代码对应的是第一种情况,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(只考虑你需要的东西)。