Kodewick
开始学 →
第 13 课,共 24 课目录

接口

第 13 课,共 24 课 · 第 3 部分 · 类型 · 约 20 分钟

长方形和圆都能算面积,算法不一样。接口(interface)说的是「能做什么」:它是一组方法。下面的 Shape 只要求一个方法 Area() float64,describe 收任何有这个方法的值:

prog.go
1package main2 3import (4	"fmt"5	"math"6)7 8type Shape interface {9	Area() float6410}11 12type Rect struct {13	Width, Height float6414}15 16func (r Rect) Area() float64 {17	return r.Width * r.Height18}19 20type Circle struct {21	Radius float6422}23 24func (c Circle) Area() float64 {25	return math.Pi * c.Radius * c.Radius26}27 28func describe(s Shape) {29	fmt.Printf("面积 %.2f\n", s.Area())30}31 32func main() {33	describe(Rect{Width: 3, Height: 4})34	describe(Circle{Radius: 1})35}
输出
面积 12.00
面积 3.14

describe 不知道、也不关心拿到的是长方形还是圆,它只调用 Area。

不用写 implements

Rect 和 Circle 从来没说过「我是 Shape」。在 Go 里,一个类型只要有接口要的全部方法,就自动实现了这个接口,不用写 implements。

接口类型的变量能装下任何实现了它的值,装的东西还能换:

prog.go
1package main2 3import (4	"fmt"5	"math"6)7 8type Shape interface {9	Area() float6410}11 12type Rect struct {13	Width, Height float6414}15 16func (r Rect) Area() float64 {17	return r.Width * r.Height18}19 20type Circle struct {21	Radius float6422}23 24func (c Circle) Area() float64 {25	return math.Pi * c.Radius * c.Radius26}27 28func main() {29	var s Shape30	s = Rect{Width: 2, Height: 5}31	fmt.Println(s.Area())32	s = Circle{Radius: 2}33	fmt.Printf("%.2f\n", s.Area())34	fmt.Printf("%T\n", s)35}
输出
10
12.57
main.Circle

%T 显示 s 里现在装的是什么类型:main.Circle(main 包里的 Circle)。

一个切片放不同的形状

[]Shape 里可以同时放长方形和圆。一个循环把它们的面积加起来:

prog.go
1package main2 3import (4	"fmt"5	"math"6)7 8type Shape interface {9	Area() float6410}11 12type Rect struct {13	Width, Height float6414}15 16func (r Rect) Area() float64 {17	return r.Width * r.Height18}19 20type Circle struct {21	Radius float6422}23 24func (c Circle) Area() float64 {25	return math.Pi * c.Radius * c.Radius26}27 28func main() {29	shapes := []Shape{30		Rect{Width: 3, Height: 4},31		Circle{Radius: 1},32		Rect{Width: 1, Height: 1},33	}34	total := 0.035	for _, s := range shapes {36		total += s.Area()37	}38	fmt.Printf("总面积 %.2f\n", total)39}
输出
总面积 16.14

以后加一种新形状(三角形),只要它有 Area,这个循环一个字都不用改。

缺了方法

少了接口要的方法,就不能当这个接口用。编译器会说缺的是哪一个:

prog.go
1package main2 3import "fmt"4 5type Shape interface {6	Area() float647}8 9type Triangle struct {10	Base, Height float6411}12 13func main() {14	var s Shape = Triangle{Base: 3, Height: 4}15	fmt.Println(s.Area())16}
输出
# command-line-arguments
./prog.go:14:16: cannot use Triangle{…} (value of struct type Triangle) as Shape value in variable declaration: Triangle does not implement Shape (missing method Area)

发生了什么: 少了方法 Area,所以没有实现接口 Shape。

Triangle does not implement Shape (missing method Area):Triangle 没有 Area 方法。给它加上 func (t Triangle) Area() float64,就能编译了。

还有一种情况:方法是指针接收者时,只有指针 *Counter 有这个方法,Counter 本身没有:

prog.go
1package main2 3import "fmt"4 5type Counter struct {6	N int7}8 9func (c *Counter) Inc() {10	c.N++11}12 13type Incrementer interface {14	Inc()15}16 17func main() {18	var c Counter19	var i Incrementer = c20	i.Inc()21	fmt.Println(c.N)22}
输出
# command-line-arguments
./prog.go:19:22: cannot use c (variable of struct type Counter) as Incrementer value in variable declaration: Counter does not implement Incrementer (method Inc has pointer receiver)

发生了什么: 方法 Inc 的接收者是指针,所以只有 *T 实现了 Incrementer:传 &x 进去。

method Inc has pointer receiver:要放进接口的是 &c。直接写 c.Inc() 没问题,因为 Go 会替变量自动取地址(第 12 课);放进接口时却不会替你取,所以要自己写 &c。这也说得通:放进去的如果是 c 的复制,Inc 改的就是复制了。

先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5type Counter struct {6	N int7}8 9func (c *Counter) Inc() {10	c.N++11}12 13type Incrementer interface {14	Inc()15}16 17func main() {18	var c Counter19	var i Incrementer = &c20	i.Inc()21	i.Inc()22	fmt.Println(c.N)23}

fmt.Stringer

上一课的 String 方法为什么管用?fmt 包里有一个接口:

type Stringer interface { String() string }

fmt.Println 拿到一个值时,先看它有没有实现 fmt.Stringer;有,就调用它的 String。你的函数也可以收 fmt.Stringer:

prog.go
1package main2 3import "fmt"4 5type Employee struct {6	Name string7	Age  int8}9 10func (e Employee) String() string {11	return fmt.Sprintf("%s(%d 岁)", e.Name, e.Age)12}13 14func introduce(s fmt.Stringer) {15	fmt.Println("这是" + s.String())16}17 18func main() {19	e := Employee{Name: "小林", Age: 28}20	fmt.Println(e)21	introduce(e)22}
输出
小林(28 岁)
这是小林(28 岁)

空接口 any

一个方法都不要求的接口是 interface{},Go 给它起了个短名字 any。每种类型都实现了它,所以 any 类型的变量什么都能装:

prog.go
1package main2 3import "fmt"4 5func main() {6	var v any7	v = 428	fmt.Printf("%v %T\n", v, v)9	v = "你好"10	fmt.Printf("%v %T\n", v, v)11	things := []any{3, "笔", 4.5, true}12	fmt.Println(things)13}
输出
42 int
你好 string
[3 笔 4.5 true]

fmt.Println 的参数就是 any,所以它什么都能显示。可是装进 any 以后,Go 只知道「这是个值」,不能直接拿它做加法:要先把里面的值取出来。

类型断言

v.(int) 叫类型断言:说「v 里装的是 int」,把它取出来。加一个 ok,就能先问再取:

prog.go
1package main2 3import "fmt"4 5func main() {6	var v any = 427	n := v.(int)8	fmt.Println(n + 1)9	s, ok := v.(string)10	fmt.Println(ok, s == "")11}
输出
43
false true

v 里不是 string,所以 ok 是 false,s 是零值 ""(s == "" 是 true)。不带 ok 时如果猜错了,程序会 panic:

prog.go
1package main2 3import "fmt"4 5func main() {6	var v any = "42"7	n := v.(int)8	fmt.Println(n)9}
输出
panic: interface conversion: interface {} is string, not int

goroutine 1 [running]:
main.main(...)
	/tmp/sandbox/prog.go:7 +0x1d
exit status 2

发生了什么: 类型断言失败:里面装的是 string,不是 int。用 v, ok := x.(T) 可以先问一问。

interface conversion: interface {} is string, not int:v 里装的是 string,不是 int。(interface {} 就是 any。)不确定里面是什么时,用带 ok 的写法。

type switch

要按里面的类型分几种情况,用 type switch。switch x := v.(type) 在每个 case 里,x 就是那种类型的值:

prog.go
1package main2 3import "fmt"4 5func describe(v any) string {6	switch x := v.(type) {7	case int:8		return fmt.Sprintf("整数,加一是 %d", x+1)9	case string:10		return fmt.Sprintf("文字,有 %d 个字节", len(x))11	case bool:12		return "真假值"13	default:14		return fmt.Sprintf("别的类型 %T", x)15	}16}17 18func main() {19	for _, v := range []any{7, "go", true, 2.5} {20		fmt.Println(describe(v))21	}22}
输出
整数,加一是 8
文字,有 2 个字节
真假值
别的类型 float64

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

让正方形也是形状

这段程序编译不过:Square 还不是 Shape。给 Square 加上 Area 方法(边长乘边长),让程序显示每个形状的面积。

要显示
6
9
25
exercise1.go
1package main2 3import "fmt"4 5type Shape interface {6	Area() float647}8 9type Rect struct {10	Width, Height float6411}12 13func (r Rect) Area() float64 {14	return r.Width * r.Height15}16 17type Square struct {18	Side float6419}20 21func main() {22	shapes := []Shape{Rect{Width: 2, Height: 3}, Square{Side: 3}, Square{Side: 5}}23	for _, s := range shapes {24		fmt.Println(s.Area())25	}26}
输出
点「运行」或按 Ctrl+Enter
练习 2

看看装的是什么

写好 kind:v 里是 int 返回 整数,是 string 返回 文字,别的都返回 其他。

要显示
整数
文字
其他
整数
exercise2.go
1package main2 3import "fmt"4 5func kind(v any) string {6	return "其他"7}8 9func main() {10	for _, v := range []any{10, "苹果", 3.5, -2} {11		fmt.Println(kind(v))12	}13}
输出
点「运行」或按 Ctrl+Enter
练习 3

只把整数加起来

写好 sumInts:把 values 里的 int 加起来,再数一数不是 int 的有几个,两个都返回。用带 ok 的类型断言。程序会算两组。

要显示
整数的和 20 别的 3
整数的和 10 别的 2
exercise3.go
1package main2 3import "fmt"4 5func sumInts(values []any) (int, int) {6	sum := 07	others := 08	return sum, others9}10 11func main() {12	sum, others := sumInts([]any{3, "笔", 4.5, 7, true, 10})13	fmt.Println("整数的和", sum, "别的", others)14	sum, others = sumInts([]any{"牛奶", 5, 5, 2.5})15	fmt.Println("整数的和", sum, "别的", others)16}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 接口是一组方法:type Shape interface { Area() float64 }。
  • 有接口要的全部方法,就自动实现了它,不用写 implements;[]Shape 能放不同的类型。
  • 缺了方法是编译错误 does not implement … (missing method …);指针接收者的方法要放 &c。
  • fmt.Println 用的是 fmt.Stringer 接口:有 String() string 就调用它。
  • any 什么都能装;v.(int) 取出里面的值,猜错会 panic,n, ok := v.(int) 不会。
  • switch x := v.(type) 按里面的类型分情况。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。