就像面向对象一样,可以给结构体(类)添加方法,结构体方法是通过接收者实现的

 

一、方法的定义


定义一个Person结构体

type Person struct {
    Name string     // Name字段
    Age  int        // Age字段
    City string     // City字段
}

那么就可以给这个结构体添加方法

// Sayname的接收者类型是Person结构体,所以是Person结构体的方法
func (person Person) Sayname() string {
    return fmt.Sprintf("Name: %v", person.Name)
}

// Infod的接收者类型是*Person结构体指针,所以是*Person结构体的方法
func (person *Person) Info string {
    return fmt.Sprintf("Name: %v, Age: %v, City: %v", person.Name, person.Age, person.City)
}

然后可以基于Person结构初始化并赋值,最后调用方法

func main() {

    // 初始化一个Person类型对象p
    p := Person{"Bob", 20, "beijing"}
// 对象调用info方法, 把变量p传给person参数 fmt.Println(p.Sayname()) // Name: Bob // 初始化一个Person类型的指针对象v v := &Person{"Alice", 35, "Shanghai"}
// 对象调用Info方法,把指针变量v传给person参数 fmt.Println(v.Info()) // Name: Alice, Age: 35, City: Shanghai }
        // 求圆的面积
        package main

        import "fmt"

        // 声明一个Circle的结构体
        type Circle struct {
            radius float64 // radius(半径)字段
        }

        // 为结构体添加一个area方法,求圆的面积
        func (c Circle) area() float64 {
            return c.radius * c.radius * 3.14
        }

        func main() {
            // 初始化结构体c
            c := Circle{5.2}
            // 调用area方法求面积
            fmt.Println(c.area()) // 84.9056
        }

        // 求矩形的面积
        package main

        import "fmt"

        // 添加一个Rect结构体
        type Rect struct {
            width, length float64
        }

        // 添加一个area方法,计算矩形的面积
        func (r *Rect) area() float64 {
            return (*r).length * (*r).width
        }

        func main() {
            // 初始化结构体变量r
            r := &Rect{1.2, 2.4}
            // 结构体变量调用area方法
            fmt.Println(r.area()) // 2.88
        }
        package main

        import "fmt"

        // 来一个结构体
        type Student struct {
            Id    int
            Name  string
            Age   int
            Score float64
        }

        // 为结构体增加info方法
        func (s *Student) info() string {
            return fmt.Sprintf("Id: [%v] Name: [%v] Age: [%v] Score: [%v]",
                (*s).Id, (*s).Name, (*s).Age, (*s).Score)
        }

        func main() {
            // 声明一个Student结构体变量
            student := &Student{1, "Bob", 25, 96.5}
            // 结构体变量调用info方法
            fmt.Println(student.info())
        }
package main

import "fmt"

type Person struct {
    Name string
    sex int
    height float64  // m
    weight float64  // kg
}

// 打招呼方法
func (p *Person) Sayhello() {
    fmt.Printf("%v: How are you.\n", p.Name)
}

// 计算BMI值
func (p *Person) Bmi() float64 {
    return p.weight / (p.height * p.height)
}

// 修改年龄方法
func (p *Person) Setheight(height float64) {
    if height < 0 {
        panic("身高必须大于0")
    }
    p.height = height
}


func main() {
    ming := &Person{"Ming", 1, 1.75, 65}
    ming.Sayhello()
    fmt.Println(ming.Bmi())
    ming.Setheight(-1)
    fmt.Println(ming.height)
}

二、方法使用细节和注意事项


1. 结构体类型是值类型, 在方法调用中遵守值类型的传递机制,是值拷贝传递方式

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// 为Person结构体实现change方法
func (p Person) change() {
    p.Name = "Alice"
    p.Age = 20
}

func main() {
    // v是Person结构变量
    v := Person{Name: "Bob", Age: 10}
    fmt.Println(v.Name, v.Age) // Bob 10  (修改前)
    // 变量v调用change方法, 将v作为实参传给p, p是v的一个值拷贝, p修改不会影响v(它们在内存是两个不同的区域)
    v.change()
    fmt.Println(v.Name, v.Age) // Alice 20 (修改后), 说明不能修改
}

2. 如果希望在方法中修改结构体变量的值,可以通过结构体指针的方式处理

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// 为Person结构体实现change方法, p是结构体指针变量
func (p *Person) change() {
    (*p).Name = "Alice"
    (*p).Age = 20
}

func main() {
    // 结构体指针变量p
    p := Person{Name: "Bob", Age: 10}
    fmt.Println(p.Name, p.Age) // Bob 10  (修改前)
    // 通过change方法修改Name和Age属性
    p.change()
    fmt.Println(p.Name, p.Age) // Alice 20 (修改后)
}

3. golang中的方法作用在指定的数据类型上(即: 和指定的数据类型绑定),因此自定义类型都可以有方法,而不仅仅是struct,比如int, float32等都可以绑定方法。 有点像扩展内置数据类型,给内置数据类型添加方法

package main

import "fmt"

// 定义一个int类型的别名integer
type integer int

// 给integer类型添加print方法
func (i integer) print() {
    fmt.Println("i =", i)
}

// 给integer类型添加add方法
func (i integer) add(x integer) integer {
    return i + x
}

func main() {
    // 声明一个integer类型的变量s
    var s integer
    s.print() // 调用print方法

    // 声明两个integer类型的变量x, y
    var x, y integer = 1, 2
    fmt.Println(x.add(y)) // 3
    fmt.Println(y.add(x)) // 3
}

4. 方法名和变量名可见性规则一样,首字母小写只能在同一个包访问,首字母大写可以在其它包访问;方法只能由该类型的变量调用

5. 如果一个结构体实现了Srting方法,那么fmt.Println默认会调用这个方法输出(有点像python中的__str__或__reper__)

    // 方式一: 通过指针
    package main

    import "fmt"

    type Person struct {
        Name string
        Age  int
    }

    // 为Person结构体实现String方法
    func (p *Person) String() string {
        return fmt.Sprintf("[Name] %v [Age] %v", (*p).Name, (*p).Age)
    }

    func main() {
        // 结构体指针变量p
        p := &Person{Name: "Bob", Age: 10}
        fmt.Println(p) // [Name] Bob [Age] 10   说明调用的是Sprintf方法
    }

    // 方式二
    package main

    import "fmt"

    type Person struct {
        Name string
        Age  int
    }

    // 为Person结构体实现String方法
    func (p Person) String() string {
        return fmt.Sprintf("[Name] %v [Age] %v", p.Name, p.Age)
    }

    func main() {
        // 结构体指针变量p
        p := Person{Name: "Bob", Age: 10}
        fmt.Println(p) // [Name] Bob [Age] 10   说明调用的是Sprintf方法
    }

三、函数与方法的使用区别


两者在传参时需要注意

对于函数:如果函数的形参要求传指针类型,那就必须传指针类型,反之亦然 (要求传什么类型就只传什么类型)

package main

import "fmt"

// 函数的参数是非指针类型,不能传指针
func test(a, b int) int {
    return a + b
}

// 函数的参数是指针类型,必须传指针
func test01(a, b *int) int {
    return *a + *b
}

func main() {
    x, y := 10, 20
    fmt.Println(test(&x, &y)) // 报错,不能传指针类型
    fmt.Println(test(x, y))   // 30  正确

    fmt.Println(test01(&x, &y)) // 正确
    fmt.Println(test01(x, y))   // 报错,需要传指针
}

对于方法:  如果方法接收者是值类型, 可以传值或指针类型,但都是按照值类型操作, 如果方法接收者是指针类型,也可以传值或指针类型,但都是按指针类型操作。也就是说最终取决于接收者的类型。这点要特别注意:

package main

import "fmt"

// 定义一个Person结构体
type Person struct {
    Name string
    Age  int
    City string
}

// 添加一个Sayname方法,接收者是值类型
func (p Person) Sayname() {
    p.Name = "Alice"
    fmt.Printf("[Name]: %v\n", p.Name)
}

// 添加一个Sayage方法,接收者是指针类型
func (p *Person) Sayage() {
    p.Age = 200
    fmt.Printf("[Age]: %v\n", p.Age)
}

func main() {
    p := Person{"Bob", 20, "Shanghai"}

    // 1. 结构体变量p调用Sayname方法,接收者类型是值类型,内部对Name字段进行修改,不会修改原先的p.Name属性
    p.Sayname() // [Name]: Alice

    // 2. 结构体指针变量p调用Sayname方法,接收者类型是指针类型,内部对Name字段进行修改,也不会修改原先的p.Name属性( 为什么? )
    // 因为接收者是值类型, 即使传指针类型当成值类型
    (&p).Sayname()      // [Name]: Alice
    fmt.Println(p.Name) // Bob
    
    结论:对于1和2调用结果是等价的,因为接收者是值类型,怎么都是按照值类型操作

    // 3. 通过结构体变量p调用Sayage方法,接收者是指针类型, 内部对Age字段修改,也会修改原先的p.Age属性( 为什么 ?)
    // p只是结构体变量,但调用时会把它转换成结构体指针(&p)
    p.Sayage() // [Age]: 200

    // 4. 通过指针变量&p调用Sayage方法,接收者是指针类型,内部对Age字段修改,也会修改原先的p.Age属性
    (&p).Sayage()      // [Age]: 200
    fmt.Println(p.Age) // 200

    结论:对于3和4调用结果是等价的,因为接收者是指针类型,怎么都是按照指针类型操作      
}

四、工厂函数


go里面没有像其它语言中的构造函数, 可以通过工厂函数来实现构造实例。是实现封装和隐藏的一种方式

package main

import "fmt"


// person结构,小写开头表示私有(封装与隐藏)
type person struct {
    Name string
    City string
    Sex int
}

func (p *person) Sayhello() {
    fmt.Println("hello: ", p.Name)
}

// 工厂函数,返回结构体类型指针
func NewPerson(name, city string, sex int) *person {
    return &person{name, city, sex}
}

func main()  {
    var ming *person
    ming = NewPerson("ming", "beijing", 23) // 通过工厂函数创建出实例
    fmt.Println(ming.Name, ming.City)
    ming.Sayhello()
}