likes
comments
collection
share

重学Go语言 | GO方法与自定义类型

作者站长头像
站长
· 阅读数 18

在面向对象编程语言中,我们可以使用类(class)来模拟现实世界的实体,通过类的属性与方法,我们可以扩展自己想要的类型。

Go语言中并没有类的概念,不过Go支持定义方法(method),Go的方法不是定义在类中的,那Go的方法定义在哪里的呢?

在这篇文章中我们就来探讨一下!

自定义数据类型

要讲清楚Go的方法,先了解Go的自定义数据类型。

Go作为一个数据类型系统,内置许多的基础数据类型供我们使用,比如intunitstringmapslice等。

如果基础数据类型还不能满足我们的需求,或者我们想和面向对象编程语言一样,定义一个有多个属性与方法的数据实体,Go语言的结构体(struct)可以达到类似的效果:

type Car struct{
    ID   int
    Band string
    Name string
}

Go语言中,通过关键词type定义的数据类型,称为自定义类型,其语法为:

type 自定义类型名称 基础数据名称

显然,结构体就是一种自定义数据类型,当然,除了结构体,我们也可以在其他内置类型的基础上创建任何的数据类型:

type Reason int
type Month int

定义好数据类型之后,就可以像使用内置数据类型一样,用自定义类型定义变量或常量了:

package main

func main(){
  const(
    Spring Reason = 1 
    Summer Reason = 2
    Autumn Reason = 3
    Winter Reason = 4
  )
  const (
    January Month = 1 + iota
    February
    March
    April
    May
    June
    July
    August
    September
    October
    November
    December
  )
}

方法的创建

Go语言的方法(method)本质是什么?简单来说就是函数(func)。

方法与函数的区别在于方法必须有一个自定义类型的接收器,在Go语言中,自定义数据类型可以通过方法来扩展功能。

方法的创建

方法本质上就是函数,所以其创建也与函数相似,只要在关键字func函数名中间加上一个用小括号括起来的接收器即可,如下图所示:

代码示例:

type User struct{
  ID   int
  Name string
}

func (u User)Say(message string){
    //...
}

func (u *User)Run(){
    //...
}

接收器的数据类型只能是使用type创建的数据类型,Go内置的数据类型不能作为接收器:

//报错,int,string等内置数据类型不能作为接收器
func (r int)String(){
  if r == 1 {
        return "春天"
    } else if r == 2 {
        return "夏天"
    } else if r == 3 {
        return "秋天"
    } else {
        return "冬天"
    }
}

同一个数据类型上不能两个相同名称的方法:

type Reason int

func (r Reason) String() string {
    if r == 1 {
        return "春天"
    } else if r == 2 {
        return "夏天"
    } else if r == 3 {
        return "秋天"
    } else {
        return "冬天"
    }
}

//报错
func (r Reason) String() string {
  
}

方法的调用

要调用方法,必须先创建对应自定义数据类型的变量,然后使用变量名后跟上一个点号来调用对应的方法:

package main

import "fmt"

type Reason int

func (r Reason) String() string {
    if r == 1 {
        return "春天"
    } else if r == 2 {
        return "夏天"
    } else if r == 3 {
        return "秋天"
    } else {
        return "冬天"
    }
}

type User struct {
    ID   int
    Name string
}

func (u User) Say(message string) {
    fmt.Println(message)
}

func main() {

    u := User{ID: 1, Name: "test"} //创建变量
    u.Say("Hello World")           //调用方法

    var reason Reason = 1

    fmt.Println(reason.String()) //输出:春天
}

方法的可见性

在面向对象编程语言中,如果不想一个方法被外部调用,可以将方法定义可见性定义为private,这就是面向对象最重要特性之一:封装。

Go语言控制可见性是通过首字母是否大小写来实现的,方法名以大写字母开头的可在包外调用,方法名以小写字母开头,则只允许包内调用:

package cart

type Cart struct {
}

func NewCart() *Cart {
    return &Cart{}
}

func (c *Cart) Lock() error {
    //...
    return nil
}

func (c *Cart) TotalPrice() (int, error) {
    //...
    return 0, nil
}

func (c *Cart) delete() error {
    //...
    return nil
}

main包中调用:

package main

import (
    "app/cart"
    "fmt"
    "log"
)

func main() {
    myCart := cart.NewCart()
    totalPrice, err := myCart.TotalPrice()
    if err != nil {
        log.Printf("impossible to compute price of the cart: %s", err)
        return
    }
    fmt.Printf("TotalPrice:%d\n", totalPrice)
    //错误,该方法不可见
    //myCart.delete()
}

接收器

接收器可以看作是方法的一个参数,但不在方法的形参列表中,而是写在方法名前面,一个方法只能有一个接收器,当通过自定义类型的变量调用方法时,Go会将调用者复制给接收器。

type User struct{
    ID   int
    FirstName string
    LastName string
}

func (u User) GetFirstName(){
    return u.FirstName //通过接收器访问当前接收器的字段
}

值接收器和指针接收器

方法的接收器有两种:值接收器和指针接收器。

前面我们的很多示例都是使用值接收器:

func (u User) GetLastName(){
    return u.FirstName //通过接收器访问当前接收器的字段
}

指针接收器的写法就是在自定义类型前面加一个*号表示指向该类型的指针:

func (u *User) GetFirstName(){
    return u.FirstName //通过接收器访问当前接收器的字段
}

值接收器与指针接收器有什么区别呢?

当通过类型变量调用方法时,会把调用者复制给接收器,无论是值接收器还是指针接收器,都会发生复制,所不同的是,使用值接收器时,会把调用者的值复制给接收器,使用指针接收器时,会把调用者的内存地址复制给接收器。

因此使用指针接收器有两个好处:

  • 当调用者变量本身数据比较大时,指针接收器可以避免大数据复制。
  • 指针接收器与调用者变量指向同一个内存地址,因此可以通过指针接收器修改调用者本身,这点值接收器是无法做到的。

下面我们通过一个示例来演示一下:

package main

import (
    "fmt"
    "strconv"
)

type Student struct {
    ID   int
    Name string
}

type ClassRoom struct {
    ID       string
    Name     string
    Students []Student
}

func (c ClassRoom) ChangeName1(name string) {
    fmt.Printf("值接收器的内存地址:%p\n", &c)
    c.Name = name
}

func (c *ClassRoom) ChangeName2(name string) {
    fmt.Printf("指针接收器的内存地址:%p\n", c)
    c.Name = name
}

func main() {
    var students []Student
    for i := 1; i <= 100; i++ {
        students = append(students, Student{ID: i, Name: "同学" + strconv.Itoa(i)})
    }
    classRoom := ClassRoom{ID: "001", Name: "高中一班", Students: students}

    fmt.Printf("调用者本身的内存地址:%p\n", &classRoom)

    classRoom.ChangeName1("高中二班")

    fmt.Println(classRoom.Name) //输出:高中一班

    classRoom.ChangeName2("高中二班")
    fmt.Println(classRoom.Name) //输出:高中二班
}

在这个示例程序中,我们创建一个ClassRoom类型的变量表示一个教室,该教室包含100个学生(Student)的信息,ChangeName1()方法使用的是值接收器,ChangeName2()方法使用的是指针接收器。

上面的示例运行结果为:

调用者本身的内存地址:0xc00005c040
值接收器的内存地址:0xc00005c080
高中一班
指针接收器的内存地址:0xc00005c040
高中二班

通过运行结果我们可以发现,使用指针接收器,接收器与调用指向同一个内存地址,这样可以修改调用者自身的属性,也可以避免大量数据的复制。

接收器的命名惯例

指针接收器的作用类似面向对象编程类的this,用于引用对象自身,不过Go并不推荐将接收器命名为this,而是推荐使用接收器类型的首字母小写:

type Reason int

//不推荐
func (this Reason)String()string{

}

type Car struct{
    ID int
    Name string
}

//推荐
func (c Car)Run(){

}

小结

与其他面向对象编程语言不同,Go的方法并不是定义在类中,而是附加于自定义类型之上的,可以更加灵活地扩展自定义数据类型的功能与行为。

最后,总结一下,阅读完这篇文章后应该掌握的几个知识点:

  • 自定义类型是什么,如何自定义数据类型
  • 方法是什么,如何创建与调用方法。
  • 接收器是什么?什么是指针接收器,什么是值接收器。
  • 什么情况下要用指针接收器。