接口

在Go语言中,接口的设计是一个典型的“鸭子类型”(Duck Typing)。如果一个结构体拥有了某个接口定义的所有方法,那么它就实现了这个接口。

先看看Java的接口(显示实现接口)

1
2
3
4
5
6
7
8
9
interface Animal{
void say();
}

class Dog implements Animal {
public void say(){
System.out.println("wang!")
}
}

类必须通过implements关键字明确申明自己实现了某个接口。

再看看Go的接口

1
2
3
4
5
6
7
8
9
type Animal interface {
Say();
}

type Dog struct{}

func (d Dog) Say(){
fmt.Println("wang!")
}

Go是隐式实现接口,只要一个类型实现了接口需求的所有方法,就自动认为他实现了该接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
func MakeSound(a Animal) {
a.Say()
}

func TestInterface1(t *testing.T) {
d := Dog{}
MakeSound(d)
// 另一种使用方法
var a Animal
// Dog 实现了(通俗的说长得像) Animal 接口,所以可以赋值给 a
a = Dog{}
a.Say()
}

Go 的接口更像是:只关心你能做什么,不关心你是谁。

任何类型,只要有Say()方法,都可以当成Animal使用。

这一块和Java差距过大(和TypeScript几乎一致),还需要一段时间适应。当时学习TS的适合,就很容易带入Java的思维,先定义接口,再写实现。对于这类语言,似乎应该先实现,然后有需要抽象的时候再提取为接口。

对于接口,主要知识点在于:

  1. Go是隐式实现接口
  2. Go类型只要拥有接口申明的所有方法,就自动实现该接口
  3. Go的接口实现是结构化类型系统的一种体现
  4. Go更强调行为抽象,而不是继承关系。

空接口

空接口是没有任何方法的接口

1
interface{}

因为它不要求任何方法,所以所有类型都实现了空接口。(Java的Object?在Go中它也叫any)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func TestEmptyInterface(t *testing.T) {
// 老古董语法了,推荐使用any别名
var x interface{}
x = 10
x = "Hello"
x = 3.1354
x = true
x = nil
x = []int{123, 456, 789}
t.Log(x)
// Go 1.18开始,引入了 any 作为 interface{} 的别名,any 更加简洁易读。
var y any
y = 10
y = "Hello"
y = 3.1354
y = true
y = nil
y = []int{123, 456, 789}
t.Log(y)
}

那么这玩意有啥用呢?相当于通用参数,对于Java程序员,下面的例子就很直观了。

1
2
3
public void say(Object obj){
System.out.println(obj.toString())
}

对应到Go

1
2
3
4
5
6
7
func PrintValue(v interface{}){
fmt.Println(v)
}
// 或者更常用的
func PrintValueAny(v any){
fmt.Println(v)
}

any,也就是空接口泛型有啥区别呢?

  • interface{} 是运行时动态类型

  • 泛型是编译期类型参数

  • interface{} 使用时通常需要类型断言

  • 泛型可以保留类型信息

类型断言

类型断言用于从接口中取出具体类型。

类型断言的语法

1
2
3
4
5
// 推荐写法
value,ok := x.(T)

// 或者 偷懒写法
value := x.(T)

下面通过一个列子来说明

1
2
3
4
5
6
7
8
9
func TestTypeAssertion(t *testing.T) {
var x any = "Hello"
s, ok := x.(string)
if ok {
t.Log(s)
} else {
t.Log("not string")
}
}

如果是偷懒写法(不安全),如果x的动态类型不是string,程序会panic

1
2
3
4
5
6
7
func TestTypeAssertionUnSafe(t *testing.T) {
var x any = 1234
// --- FAIL: TestTypeAssertionUnSafe (0.00s)
// panic: interface conversion: interface {} is int, not string [recovered, repanicked]
s := x.(string)
t.Log(s)
}

所以,推荐使用第一种写法,更安全,更可靠。

类型 switch

当一个接口可能有多种类型时,可以用 type switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func PrintType(v any) {
switch value := v.(type) {
case string:
fmt.Printf("string: %v\n", value)
case int:
fmt.Printf("int: %v\n", value)
case bool:
fmt.Printf("bool: %v\n", value)
default:
fmt.Printf("other: %T\n", value)
}
}

type Class struct {
Name string
}

func TestTypeSwitch(t *testing.T) {
PrintType("Hello")
PrintType(true)
PrintType(123)
// other: day5.Class
PrintType(Class{Name: "Java"})
//other: float64
PrintType(3.45)
}

在第四章的switch介绍时,顺便也加入了这个特性,这里就再啰嗦一下,毕竟既是编码常用,也是面试重点:

v.(type) 只能用于 switch 中,不能单独使用。

结构体嵌入(组合)

Go 没有传统继承

普通组合

对于Java来说,存在一个类与类之间的关系,包括继承、组合、聚合之类的关系。但是Go就只有组合。

1
2
3
4
5
class Animal {
void eat(){}
}
// 继承关系
class Dog extends Animal{}

如果是Go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Engine struct{}

func (e Engine) Start() {
fmt.Println("start...")
}

type Car struct {
engine Engine
}

func TestComposition(t *testing.T) {
car := Car{engine: Engine{}}
car.engine.Start()
}

上述这个例子从称之为普通组合

结构体嵌入

Go 特有的一种组合方式,通常被称为 Struct Embedding(结构体嵌套/内嵌)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Engine struct{}

func (e Engine) Start() {
fmt.Println("start...")
}

type Tesla struct {
Engine
}

func TestComposition1(t *testing.T) {
car := Tesla{Engine{}}
car.Start()
}

看起来像继承,但本质是组合。

上述例子可行的原因是:方法提升

当结构体嵌入另一个结构体时,被嵌入类型的方法会被提升。现在再通过一个类似的例子说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Animal1 struct{}

func (a Animal1) Eat() {
fmt.Println("eat....")
}

type Dog1 struct {
Animal1
}

func TestComposition2(t *testing.T) {
d := Dog1{}
d.Eat()
}

Dog1 可以直接调用 Eat(),但不是继承。本质上相当于

1
d.Animal.Eat()

当然对于组合来说,Java存在的问题,Go肯定也会存在。那就是如果两个结构体的字段名称和类型一模一样,那么该如何处理呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type A struct {
Name string
}

type B struct {
Name string
}

type C struct {
A
B
}

func TestComposition3(t *testing.T) {
c := C{}
//不明确的引用 'Name'
//fmt.Println(c.Name)
fmt.Println(c.A.Name)
fmt.Println(c.B.Name)
}

遇到这种情况时,就必须明确指定,访问的是谁的Name

这个特性应该会比较常用,例如Java中,会讲一些固定的字段抽取出来放在Base中,其他类只需要继承这个类就可以复用基础字段,对于Go也可以,但不是继承,而是组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type BaseModel struct {
ID int
CreatedAt time.Time
UpdatedAt time.Time
}

type user struct {
BaseModel
Name string
}

func TestComposition4(t *testing.T) {
u := user{Name: "Tom", BaseModel: BaseModel{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}}
fmt.Println(u.ID)
fmt.Println(u.CreatedAt)
}

Favor composition over inheritance (组合优于继承)

对于面试来说,只需要记住一点:

Go没有传统继承,它使用结构体嵌入和接口实现来完成代码复用和多态。

方法接收者(Receiver)

Go 没有 Java 中的类方法语法,但可以给类型定义方法。例如:

1
2
3
4
5
6
7
type User struct {
Name string
}

func (u User) SayHello(){
fmt.Println("Hello",u.Name)
}

这里的(u User)就是方法接收者。类似的Java代码如下:

1
2
3
4
5
class User {
void sayHello(){
System.out.println(this.name);
}
}

在Go语言中,方法接收者有两种:值接收者指针接收者

1
2
3
4
// 值接收者
func (u User) Method()
// 指针接收者
func(u *User) Method()

值接收者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type User struct {
Name string
}

// 这里会复制一个对象
func (u User) Rename(name string) {
u.Name = name
}

// 加了 (u User/*User) 就是这个结构体的方法
func (u User) Add(a, b int) int {
return a + b
}

func TestReceiver(t *testing.T) {
u := User{Name: "Tom"}
u.Rename("Jerry")
// Tom
t.Log(u.Name)
}

这里修改名字是不会生效的,因为对于值接收者,他是复制一个对象,然后再去修改。日常编码中,这种方式使用的频率很低。

指针接受者

如果想要修改原对象里的某个值,那就需要传入指针,这也是日常编码中最常用的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type User struct {
Name string
}

func (u *User) RenamePointer(name string) {
u.Name = name
}

// 加了 (u User/*User) 就是这个结构体的方法
func (u User) Add(a, b int) int {
return a + b
}

func TestReceiver(t *testing.T) {
u := User{Name: "Tom"}
u.RenamePointer("Tony")
t.Log(u.Name)
}

对于Java程序员来说,稍微有些区别,需要特别注意这一点。

这两个特性其实在前期学习slicemap函数等地方已经提前接触过了。

下面再温故而知新,复述一下,使用场景:

使用指针接收者的场景

方法需要修改接收者

1
2
3
4
// 例如
func (u *User) RenamePointer(name string) {
u.Name = name
}

结构体较大,避免复制成本

1
2
3
4
5
type BigStruct struct {
Data [1024]int
}

func (b *BigStruct) Process(){}

保持方法集一致

如果一个类型有些方法用了指针接收者,通常建议其他方法也是用指针接收者。

1
2
3
func (u *User) SetName(name string){}

func (u *User) GetName() string{}

这样可以避免接口实现时出现混乱。

使用值接收者的场景

小对象且不可变语义

1
2
3
4
5
6
7
8
type Point struct {
X int
Y int
}

func (p Point) Distance() float64 {
return math.Sqrt(float64(p.X*p.X + p.Y * p.Y))
}

基础类型别名

1
2
3
4
5
type MyInt int

func (m MyInt) IsPositive() bool {
return m > 0
}

不希望方法修改原始对象

1
2
3
func (u User) Display(){
fmt.Println(u.Name)
}

方法调用时得自动取址

Go语言有一个语法糖:

1
2
user := User{}
user.SetName("Tom")

如果SetName是指针接收者

1
func (u *User) SetName(name string){}

Go语言会自动转换为

1
(&user).SetName("Tom")

但是,这只在变量可寻址时有效。

不可寻址得情况:

1
2
// 无法在 'User{}' 中调用指针方法
// User{}.SetName("Tom")

如果SetName是市政接收者,这通常不允许,因为临时值不可获取地址。

方法集与接口实现

换一种说法就是: T *T 的方法集。

对于值接受者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Speaker interface {
Speak()
}

type Cat struct{}

func (cat Cat) Speak() {
fmt.Println("wangwang")
}

func TestInterface(t *testing.T) {
var s Speaker
cat := Cat{}
// 这样可以
s = cat
s.Speak()
// 这样也可以
s = &cat
s.Speak()
}

因为值接收者方法属于:

  • Cat的方法集
  • *Cat的方法集

对于指针接收者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Bird struct{}

func (bird *Bird) Speak() {
fmt.Println("jijizhazha")
}

func TestInterface2(t *testing.T) {
var s Speaker
bird := Bird{}
// 这样可以
s = &bird
s.Speak()
// 这样不可以
//无法将 bird (类型 Bird) 用作类型 Speaker
//类型未实现 Speaker,因为 Speak 方法有指针接收器
//s = bird
//s.Speak()
}

因为指针接收者方法只属于*Bird的方法集,不属于Bird的方法集。

总结下,值接收者指针接受者的区别:

  • 值接收者会复制接收者
  • 指针接收者传递地址
  • 指针接收者可以修改原来的对象
  • 大结构体通常用指针接收者,避免复制
  • 方法集不同,会影响接口实现

综合案例

下面通过一个综合案例,将上面的知识点都串联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package day5

import (
"errors"
"fmt"
"testing"
)

type UserInfo struct {
ID int64
Name string
}

// 知识点1:接口定义
type UserRepository interface {
FindByID(id int64) (*UserInfo, error)
Save(user *UserInfo) error
}

type MemoryUserRepository struct {
data map[int64]*UserInfo
}

func NewMemoryUserRepository() *MemoryUserRepository {
return &MemoryUserRepository{
data: make(map[int64]*UserInfo),
}
}

// 知识点2: 隐式实现
func (r *MemoryUserRepository) FindByID(id int64) (*UserInfo, error) {
user, ok := r.data[id]
if !ok {
return nil, errors.New("not found")
}
return user, nil
}

// 知识点2: 隐式实现,知识点3:指针接收者
func (r *MemoryUserRepository) Save(user *UserInfo) error {
r.data[user.ID] = user
return nil
}

type Logger struct{}

func (log Logger) Info(message string) {
fmt.Println("[INFO]", message)
}

// 知识点4:结构体嵌入,知识点5组合复用
type UserService struct {
repo UserRepository
Logger
}

func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
}
}

func (s *UserService) CreateUser(id int64, name string) error {
s.Info("Creating user...")
user := &UserInfo{
ID: id,
Name: name,
}
return s.repo.Save(user)
}

func (s *UserService) GetUser(id int64) (*UserInfo, error) {
return s.repo.FindByID(id)
}

func TestDay5(t *testing.T) {
repo := NewMemoryUserRepository()
service := NewUserService(repo)

err := service.CreateUser(1, "John")
if err != nil {
panic(err)
}

user, err := service.GetUser(1)
if err != nil {
panic(err)
}
fmt.Println(user.Name)
}