接口
长方形和圆都能算面积,算法不一样。接口(interface)说的是「能做什么」:它是一组方法。下面的 Shape 只要求一个方法 Area() float64,describe 收任何有这个方法的值:
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。
接口类型的变量能装下任何实现了它的值,装的东西还能换:
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 里可以同时放长方形和圆。一个循环把它们的面积加起来:
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,这个循环一个字都不用改。
缺了方法
少了接口要的方法,就不能当这个接口用。编译器会说缺的是哪一个:
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 本身没有:
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}
i 里装的是指向 c 的指针,i.Inc() 改的就是 c。调了两次,所以是 2。
fmt.Stringer
上一课的 String 方法为什么管用?fmt 包里有一个接口:
type Stringer interface { String() string }
fmt.Println 拿到一个值时,先看它有没有实现 fmt.Stringer;有,就调用它的 String。你的函数也可以收 fmt.Stringer:
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 类型的变量什么都能装:
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,就能先问再取:
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:
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 就是那种类型的值:
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
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
让正方形也是形状
✓这段程序编译不过:Square 还不是 Shape。给 Square 加上 Area 方法(边长乘边长),让程序显示每个形状的面积。
6 9 25
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}
看看装的是什么
✓写好 kind:v 里是 int 返回 整数,是 string 返回 文字,别的都返回 其他。
整数 文字 其他 整数
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}
只把整数加起来
✓写好 sumInts:把 values 里的 int 加起来,再数一数不是 int 的有几个,两个都返回。用带 ok 的类型断言。程序会算两组。
整数的和 20 别的 3 整数的和 10 别的 2
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}
这一课学会了
- 接口是一组方法:
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)按里面的类型分情况。