Bootstrap

Go 方法、接口与面向对象

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
}

这里MyIntMyFloat都定义了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()
}
代码执行流程
  1. Greeter{Name: "John"}创建了一个Greeter实例,Name字段为"John"
  2. .GreetGreet方法与Greeter实例绑定,生成一个方法值,并赋值给变量greet
  3. greet()调用方法值,相当于执行Greeter{Name: "John"}.Greet()
  4. 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()
    }
}

HumanStudentEmployee类型都实现了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()
}
  1. 接口定义
  • Human是一个接口,定义了Len()方法。
  • Student是一个嵌套了Human接口的接口。这意味着任何实现了Student接口的类型,必须同时实现Human接口的Len()方法。
  1. 类型实现
  • 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

&p1p1 的指针(即 *Person 类型)。
由于 Person 实现了 Describer 接口,因此 *Person 也实现了 Describer 接口。
&p1 赋值给 d1 是合法的,因为 d1Describer 接口类型,而 *Person 实现了 Describer 接口。

通过接口调用 Describe() 方法
d1.Describe()

d1Describer 接口类型,它存储了 *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 接口嵌入了 ReaderWriter 接口,一个实现了 ReadWriter 接口的类必须实现所有 ReaderWriter 接口中定义的方法。

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 中,多态是通过接口实现的。任何结构体只要实现了接口的所有方法,都被认为实现了该接口。这意味着,可以用接口类型的变量来持有这些结构体的实例,并调用它们的方法。

请添加图片描述

;