文章目录
Go方法、接口与面向对象
方法
- 方法是与特定类型关联的函数。它需要一个接收者(Receiver),接收者可以是结构体、自定义类型或基本类型的别名。
方法的特点
- 必须与某个类型绑定。
- 通过类型的实例调用。
- 接收者是方法的第一个参数,可以是值类型或指针类型。
指针接收者和值接收者的区别仍然适用。如果方法需要修改接收者的状态,必须使用指针接收者。
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
// 定义一个方法,计算矩形的面积
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
area := rect.Area() // 调用方法
fmt.Println("Area:", area) // 输出: Area: 50
}
值接收者
操作接收者的副本,不会修改原始数据。
适用于不需要修改接收者状态的场景。
func(r Rectangle)Area()float64{
return r.Width * r.Height
}
指针接收者
操作接收者本身,可以修改原始数据。
适用于需要修改接收者状态的场景。
func(r *Rectangle)SetWidth(newWidth float64){
r.Width = newWidth
}
非结构体类型的接收者示例
示例 1:基本类型的别名
你可以为基本类型(如int、string等)定义别名,并为这些别名类型定义方法。
package main
import"fmt"
// 定义一个 int 的别名类型
type MyInt int
// 为 MyInt 类型定义一个方法
func(m MyInt)IsPositive()bool{
return m >0
}
func main(){
var num MyInt =10
fmt.Println(num.IsPositive())// 输出: true
}
示例 2:切片类型
你可以为切片类型定义方法。
package main
import"fmt"
// 定义一个字符串切片的别名类型
type StringSlice []string
// 为 StringSlice 类型定义一个方法
func(s StringSlice)Join(sep string)string{
result :=""
for i, v :=range s {
if i >0{
result += sep
}
result += v
}
return result
}
func main(){
slice := StringSlice{"a","b","c"}
fmt.Println(slice.Join("-"))// 输出: a-b-c
}
示例 3:函数类型
你甚至可以为函数类型定义方法。
package main
import"fmt"
// 定义一个函数类型
type MyFunc func(int)int
// 为 MyFunc 类型定义一个方法
func(f MyFunc)CallAndDouble(x int)int{
return f(x)*2
}
func main(){
// 定义一个函数
square :=func(x int)int{
return x * x
}
// 将函数转换为 MyFunc 类型
myFunc :=MyFunc(square)
// 调用方法
result := myFunc.CallAndDouble(3)
fmt.Println(result)// 输出: 18 (3*3=9, 9*2=18)
}
示例 4:映射类型
你可以为映射类型定义方法。
package main
import"fmt"
// 定义一个映射类型的别名
type MyMap map[string]int
// 为 MyMap 类型定义一个方法
func(m MyMap)HasKey(key string)bool{
_, exists := m[key]
return exists
}
func main(){
m := MyMap{"a":1,"b":2}
fmt.Println(m.HasKey("a"))// 输出: true
fmt.Println(m.HasKey("c"))// 输出: false
}
Go 语言中的方法名规则
同一类型中不能有同名方法
在同一个类型中,方法名必须唯一,不能定义多个同名方法,即使参数不同也不行。
例如,以下代码会报错:
type MyInt int
func(m MyInt)Add(a int)int{
return int(m)+ a
}
func(m MyInt)Add(a, b int)int{// 错误:重复的方法名
return int(m)+ a + b
}
不同类型可以有同名方法
不同的类型可以定义同名的方法,因为这些方法属于不同的接收者。
例如:
type MyInt int
type MyFloat float64
func(m MyInt)Add(a int)int{
return int(m)+ a
}
func(f MyFloat)Add(a float64)float64{
return float64(f)+ a
}
这里MyInt
和MyFloat
都定义了Add
方法,但由于接收者类型不同,它们是合法的方法。
method继承
method是可以继承的,如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
method重写
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//Human定义method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
方法值
方法值是将方法与特定接收者绑定的函数。
方法值可以像普通函数一样被调用、存储和传递。
type Greeter struct {
Name string
}
func (g Greeter) Greet() {
fmt.Println("Hello", g.Name)
}
func main() {
greet := Greeter{Name: "John"}.Greet //这一句就是把结构体和方法绑定得到方法值并赋予greet
greet()
}
代码执行流程
Greeter{Name: "John"}
创建了一个Greeter
实例,Name
字段为"John"
。.Greet
将Greet
方法与Greeter
实例绑定,生成一个方法值,并赋值给变量greet
。greet()
调用方法值,相当于执行Greeter{Name: "John"}.Greet()
。Greet
方法打印"Hello John"
。
方法值的特点
- 绑定接收者:
方法值会将接收者(Greeter
实例)和方法(Greet
)绑定在一起。
在调用方法值时,接收者的值会被隐式传递。 - 延迟调用:
方法值可以存储起来,稍后再调用。 - 类似于闭包:
方法值捕获了接收者的值,类似于闭包捕获外部变量。
方法值的应用场景
延迟调用
将方法值存储起来,稍后再调用。
例如:
func main(){
g := Greeter{Name:"Alice"}
greetLater := g.Greet
// 稍后调用
greetLater()// 输出: Hello Alice
}
作为函数参数传递
方法值可以作为函数参数传递。
例如:
func callLater(f func()){
f()
}
func main(){
g := Greeter{Name:"Bob"}
callLater(g.Greet)// 输出: Hello Bob
}
实现回调机制
方法值可以用于实现回调机制。
例如:
func main(){
g := Greeter{Name:"Charlie"}
time.AfterFunc(2*time.Second, g.Greet)// 2秒后调用
time.Sleep(3* time.Second)// 等待输出
}
方法的可见性
和字段或函数一样,如果方法的第一个字母是大写的,那么这个方法可以被这个包以外的代码访问。
接口
在Go中,接口是一组方法签名。当类型为接口中的所有方法提供定义时,它被称为实现接口。它与OOP非常相似。接口指定了类型应该具有的方法,类型决定了如何实现这些方法。
简例
例一:简述
type Sayer interface {
Say() string
}
type Dog struct {
}
func (d Dog) Say() string {
return "Woof!"
}
type Cat struct {
}
func (c Cat) Say() string {
return "Meow!"
}
func AnimalTalk(s Sayer) {
fmt.Println(s.Say())
}
func main() {
var d Sayer = Dog{}
var c Sayer = Cat{}
AnimalTalk(d)
}
例二:接口灵活性体现
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
loan float32
}
type Employee struct {
Human //匿名字段
company string
money float32
}
//Human实现Sayhi方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Human实现Sing方法
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
//Employee重写Human的SayHi方法
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}
//定义Men类型的变量i
var i Men
//i能存储Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i也能存储Employee
i = Tom
fmt.Println("This is Tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//定义了slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//这三个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x {
value.SayHi()
}
}
Human
、Student
和Employee
类型都实现了SayHi()
和Sing(lyrics string)
方法,因此它们都实现了Men
接口。
接下来定义了一个Men
类型的变量i
。这个变量i
可以存储任何实现了Men
接口的类型的值。
可以使用i
来调用SayHi()
和Sing()
方法,而不需要关心i
具体是Student
还是Employee
类型。这就是接口的威力所在——它允许你编写更通用和灵活的代码。
例三:接口嵌套
package main
import "fmt"
type Human interface {
Len()
}
type Student interface {
Human
}
type Test struct {
}
func (h *Test) Len() {
fmt.Println("成功")
}
func main() {
var s Student
s = new(Test)
s.Len()
}
- 接口定义
Human
是一个接口,定义了Len()
方法。Student
是一个嵌套了Human
接口的接口。这意味着任何实现了Student
接口的类型,必须同时实现Human
接口的Len()
方法。
- 类型实现
Test
结构体实现了Len()
方法,因此它实现了Human
接口。- 由于
Student
接口嵌套了Human
接口,而Test
实现了Human
接口,因此Test
也隐式地实现了Student
接口。
3.接口变量赋值 s
是一个Student
接口类型的变量。new(Test)
创建了一个Test
类型的实例,并将其赋值给s
。由于Test
实现了Student
接口,因此这是合法的。
4.方法调用s.Len()
调用了Test
类型的Len()
方法,输出"成功"
。
5.解释- Go 语言的接口是隐式实现的。只要一个类型实现了接口中定义的所有方法,它就被认为是实现了该接口。
Test
类型实现了Len()
方法,因此它实现了Human
接口。- 由于
Student
接口嵌套了Human
接口,Test
也自动实现了Student
接口。
鸭子类型
-
Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过接口的方式完美支持鸭子类型。
-
而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口,之后,才能用在任何需要这个接口的地方。如果你在程序中调用某个数,却传入了一个根本就没有实现另一个的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。
-
Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。
接口类型断言
- 类型断言(Type Assertion)是 Go 语言中用于将接口类型转换为具体类型的操作。由于 Go 的接口是动态类型的,接口变量在运行时可以存储任何实现了该接口的类型的值。类型断言允许我们在运行时检查接口变量的实际类型,并将其转换为指定的具体类型。
为什么需要类型断言
- 在 Go 中,接口(interface)是一种类型,它定义了一组方法签名,但不实现这些方法。一个类型只要实现了接口中的所有方法,就自动实现了该接口。接口变量可以存储任何实现了该接口的类型的值。
类型断言的用途
- 检查接口变量的实际类型:
当我们不确定接口变量的实际类型时,可以使用类型断言来检查。
var i interface{} = "hello"
if s, ok := i.(string); ok {
fmt.Println("It's a string:", s)
} else {
fmt.Println("It's not a string")
}
- 将接口类型转换为具体类型:
我们知道接口变量的实际类型时,可以使用类型断言将其转换为具体类型,以便进行更具体的操作。
var i interface{} = 42
if n, ok := i.(int); ok {
fmt.Println("It's an int:", n+1)
}
- 处理空接口:
接口interface{}
可以存储任何类型的值。类型断言常用于处理空接口。
func printValue(v interface{}) {
switch v.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Int:", v)
default:
fmt.Println("Unknown type")
}
}
因为空接口 interface{}
没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{}
,那么在函数中,需要对形参进行断言,从而得到它的真实类型。
例子
package main
import "fmt"
type Student struct {
}
func main() {
// 不安全的类型断言,如果失败会直接 panic
var i1 interface{} = new(Student)
s := i1.(Student)
fmt.Println(s)
// 安全的类型断言
var i2 interface{} = new(Student)
s2, ok := i2.(Student)
if ok {
fmt.Println(s2)
} else {
fmt.Println("Type assertion failed for i2")
}
}
接口和指针
如果一个接口类型的方法集合包含使用值接收者定义的方法,那么这个接口可以存储这个指针类型的值。
type Describer interface {
Describe()
}
type Person struct {
name string
}
func (p Person) Describe() {
fmt.Println("I am a person.")
}
func main() {
var d1 Describer
p1 := Person{"Sam"}
d1 = &p1
d1.Describe()
}
定义接口变量 d1
var d1 Describer
d1
是一个 Describer
接口类型的变量。它可以存储任何实现了 Describer
接口的类型的值。
创建 Person
实例 p1
p1 := Person{"Sam"}
这里创建了一个 Person
类型的实例 p1
,并初始化其 name
字段为 "Sam"
。
将 p1
的地址赋值给 d1
d1 = &p1
&p1
是 p1
的指针(即 *Person
类型)。
由于 Person
实现了 Describer
接口,因此 *Person
也实现了 Describer
接口。
将 &p1
赋值给 d1
是合法的,因为 d1
是 Describer
接口类型,而 *Person
实现了 Describer
接口。
通过接口调用 Describe()
方法
d1.Describe()
d1
是 Describer
接口类型,它存储了 *Person
类型的值(即 &p1
)。
调用 d1.Describe()
时,实际上调用的是 Person
类型的 Describe()
方法。
因此,程序会输出:I am a person.
接口嵌套和方法冲突
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
在这个例子中,ReadWriter
接口嵌入了 Reader
和 Writer
接口,一个实现了 ReadWriter
接口的类必须实现所有 Reader
和 Writer
接口中定义的方法。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
Read() // 与 Reader 接口的 Read 方法冲突
}
type ReadCloser interface {
Reader
Closer
}
面向对象
New() 函数替代构造函数
Go 中没有构造函数,但我们可以手动写一个 New
函数实现类似构造函数的功能。
package employee
import (
"fmt"
)
type employee struct {
firstName string
lastName string
totalLeaves int
leavesTaken int
}
func New(firstName string, lastName string, totalLeave int, leavesTaken int) employee {
e := employee {firstName, lastName, totalLeave, leavesTaken}
return e
}
func (e employee) LeavesRemaining() {
fmt.Printf("%s %s has %d leaves remaining", e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}
如果有部分值不确定,可以通过默认值 + 结构体选项模式来实现可选参数。
package main
import "fmt"
type employee struct {
firstName string
lastName string
totalLeaves int
leavesTaken int
}
// Option 是一个函数类型,用于修改 employee 结构体的字段
type Option func(*employee)
// WithTotalLeaves 是一个 Option 函数,用于设置 totalLeaves 字段
func WithTotalLeaves(totalLeaves int) Option {
return func(e *employee) {
e.totalLeaves = totalLeaves
}
}
// WithLeavesTaken 是一个 Option 函数,用于设置 leavesTaken 字段
func WithLeavesTaken(leavesTaken int) Option {
return func(e *employee) {
e.leavesTaken = leavesTaken
}
}
// New 函数用于创建 employee 结构体实例
func New(firstName, lastName string, options ...Option) employee {
e := employee{
firstName: firstName,
lastName: lastName,
totalLeaves: 30, // 默认值
leavesTaken: 0, // 默认值
}
// 应用所有的选项
for _, opt := range options {
opt(&e)
}
return e
}
func main() {
// 提供所有值创建实例
emp1 := New("John", "Doe", WithTotalLeaves(20), WithLeavesTaken(5))
fmt.Printf("Employee 1: %+v\n", emp1)
// 部分值不确定,使用默认值创建实例
emp2 := New("Jane", "Smith")
fmt.Printf("Employee 2: %+v\n", emp2)
}
关键点
- 结构体替代类:Go 语言没有类的概念,但结构体可以起到相同的作用。可以在结构体上定义方法,从而模拟类的行为。
- 构造函数的替代:Go 不支持构造函数。但可以提供一个
New()
函数,来初始化并返回一个结构体的实例。 - 组合替代继承:Go 不支持继承。但可以通过嵌入结构体的方式实现组合,从而达到类似继承的效果。
- 多态性:在 Go 中,多态是通过接口实现的。任何结构体只要实现了接口的所有方法,都被认为实现了该接口。这意味着,可以用接口类型的变量来持有这些结构体的实例,并调用它们的方法。