接口 在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 a = Dog{} a.Say() }
Go 的接口更像是:只关心你能做什么,不关心你是谁。
任何类型,只要有Say()方法,都可以当成Animal使用。
这一块和Java差距过大(和TypeScript几乎一致),还需要一段时间适应。当时学习TS的适合,就很容易带入Java的思维,先定义接口,再写实现。对于这类语言,似乎应该先实现,然后有需要抽象的时候再提取为接口。
对于接口,主要知识点在于:
Go是隐式实现接口
Go类型只要拥有接口申明的所有方法,就自动实现该接口
Go的接口实现是结构化类型系统的一种体现
Go更强调行为抽象,而不是继承关系。
空接口 空接口是没有任何方法的接口
因为它不要求任何方法,所以所有类型都实现了空接口。(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) { var x interface {} x = 10 x = "Hello" x = 3.1354 x = true x = nil x = []int {123 , 456 , 789 } t.Log(x) 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 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 ) PrintType(Class{Name: "Java" }) PrintType(3.45 ) }
在第四章的switch介绍时,顺便也加入了这个特性,这里就再啰嗦一下,毕竟既是编码常用,也是面试重点:
v.(type) 只能用于 switch 中,不能单独使用。
结构体嵌入(组合)
普通组合 对于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(),但不是继承。本质上相当于
当然对于组合来说,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{} 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 } func (u User) Add(a, b int ) int { return a + b } func TestReceiver (t *testing.T) { u := User{Name: "Tom" } u.Rename("Jerry" ) 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 } 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程序员来说,稍微有些区别,需要特别注意这一点。
这两个特性其实在前期学习slice、map、函数等地方已经提前接触过了。
下面再温故而知新,复述一下,使用场景:
使用指针接收者的场景 方法需要修改接收者 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语言会自动转换为
但是,这只在变量可寻址时有效。
不可寻址得情况:
如果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() }
因为值接收者方法属于:
对于指针接收者:
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的方法集。
总结下,值接收者和指针接受者的区别:
值接收者会复制接收者
指针接收者传递地址
指针接收者可以修改原来的对象
大结构体通常用指针接收者,避免复制
方法集不同,会影响接口实现
综合案例 下面通过一个综合案例,将上面的知识点都串联起来。
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 day5import ( "errors" "fmt" "testing" ) type UserInfo struct { ID int64 Name string } 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), } } func (r *MemoryUserRepository) FindByID(id int64 ) (*UserInfo, error ) { user, ok := r.data[id] if !ok { return nil , errors.New("not found" ) } return user, nil } 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) } 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) }