Golang通脈之方法


方法和接收者

Go語言中的方法(Method)是一種作用於特定類型變量的函數。這種特定類型變量叫做接收者(Receiver)。接收者的概念就類似於其他語言中的this或者 self

Go 語言中同時有函數和方法。一個方法就是一個包含了接受者的函數,接受者可以是命名類型或者結構體類型的一個值或者是一個指針。所有給定類型的方法屬於該類型的方法集

方法只是一個函數,它帶有一個特殊的接收器類型,它是在func關鍵字和方法名之間編寫的。接收器可以是struct類型或非struct類型。接收方可以在方法內部訪問。

方法能給用戶自定義的類型添加新的行為。它和函數的區別在於方法有一個接收者,給一個函數添加一個接收者,那么它就變成了方法。接收者可以是值接收者,也可以是指針接收者

在調用方法的時候,值類型既可以調用值接收者的方法,也可以調用指針接收者的方法;指針類型既可以調用指針接收者的方法,也可以調用值接收者的方法。

也就是說,不管方法的接收者是什么類型,該類型的值和指針都可以調用,不必嚴格符合接收者的類型。

方法的定義格式如下:

func (t Type) methodName(parameter)(return) {
  
}

其中,

  • t:接收者中的參數變量名在命名時,官方建議使用接收者類型名稱首字母的小寫,而不是selfthis之類的命名。
  • Type:接收者類型和參數類似,可以是指針類型和非指針類型。
  • methodNameparameterreturn :具體格式與函數定義相同。
//Person 結構體
type Person struct {
	name string
	age  int8
}

//NewPerson 構造函數
func NewPerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

//Dream Person做夢的方法
func (p Person) Dream() {
	fmt.Printf("%s的夢想是學好Go語言!\n", p.name)
}

func main() {
	p1 := NewPerson("張三", 25)
	p1.Dream()
}

方法與函數的區別是,函數不屬於任何類型,方法屬於特定的類型。

可以定義相同的方法名

type Rectangle struct {
	width, height float64
}
type Circle struct {
	radius float64
}


func (r Rectangle) area() float64 {
	return r.width * r.height
}
//該 method 屬於 Circle 類型對象中的方法
func (c Circle) area() float64 {
	return c.radius * c.radius * math.Pi
}
func main() {
	r1 := Rectangle{12, 2}
	r2 := Rectangle{9, 4}
	c1 := Circle{10}
	c2 := Circle{25}
	fmt.Println("Area of r1 is: ", r1.area())
	fmt.Println("Area of r2 is: ", r2.area())
	fmt.Println("Area of c1 is: ", c1.area())
	fmt.Println("Area of c2 is: ", c2.area())
}

運行結果

Area of r1 is:  24
Area of r2 is:  36
Area of c1 is:  314.1592653589793
Area of c2 is:  1963.4954084936207
  • 雖然method的名字一模一樣,但是如果接收者不一樣,那么method就不一樣
  • method里面可以訪問接收者的字段
  • 調用method通過.訪問,就像struct里面訪問字段一樣

指針類型的接收者

指針類型的接收者由一個結構體的指針組成,由於指針的特性,調用方法時修改接收者指針的任意成員變量,在方法結束后,修改都是有效的。這種方式就十分接近於其他語言中面向對象中的this或者self

type Rectangle struct {
	width, height int
}

func (r *Rectangle) setVal() {
	r.height = 20
}

func main() {
	p := Rectangle{1, 2}
	s := p
	p.setVal()
	fmt.Println(p.height, s.height)
}

結果

20 2

值類型的接收者

方法作用於值類型接收者時,Go語言會在代碼運行時將接收者的值復制一份。在值類型接收者的方法中可以獲取接收者的成員值,但修改操作只是針對副本,無法修改接收者變量本身

type Rectangle struct {
	width, height int
}

func (r Rectangle) setVal() {
	r.height = 20
}

func main() {
	p := Rectangle{1, 2}
	s := p
	p.setVal()
	fmt.Println(p.height, s.height) 	// 2 2
}

什么時候應該使用指針類型接收者

  1. 需要修改接收者中的值
  2. 接收者是拷貝代價比較大的大對象
  3. 保證一致性,如果有某個方法使用了指針接收者,那么其他的方法也應該使用指針接收者。

方法和函數

已經有了函數,為什么還要使用方法?

type Employee struct {  
    name     string
    salary   int
    currency string
}

/*
 displaySalary() method converted to function with Employee as parameter
*/
func displaySalary(e Employee) {  
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {  
    emp1 := Employee{
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    displaySalary(emp1)
}

在上面的程序中,displaySalary方法被轉換為一個函數,而Employee struct作為參數傳遞給它。這個程序也產生了相同的輸出:Salary of Sam Adolf is $5000.。

為什么可以用函數來寫相同的程序呢?有以下幾個原因:

  1. Go不是一種純粹面向對象的編程語言,它不支持類。因此,類型的方法是一種實現類似於類的行為的方法。
  2. 相同名稱的方法可以在不同的類型上定義,而具有相同名稱的函數是不允許的。

任意類型添加方法

在Go語言中,接收者的類型可以是任何類型,不僅僅是結構體,任何類型都可以擁有方法。 舉個例子,我們基於內置的int類型使用type關鍵字可以定義新的自定義類型,然后為我們的自定義類型添加方法。

//MyInt 將int定義為自定義MyInt類型
type MyInt int

//SayHello 為MyInt添加一個SayHello的方法
func (m MyInt) SayHello() {
	fmt.Println("Hello, 我是一個int。")
}
func main() {
	var m1 MyInt
	m1.SayHello() //Hello, 我是一個int。
	m1 = 100
	fmt.Printf("%#v  %T\n", m1, m1) //100  main.MyInt
}

注意事項: 非本地類型不能定義方法,也就是說我們不能給別的包的類型定義方法。

方法繼承

方法是可以繼承的,如果匿名字段實現了一個方法,那么包含這個匿名字段的struct也能調用該方法

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()
}

運行結果:

Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam you can call me on 111-888-XXXX

方法重寫

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()
}

運行結果:

Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam, I work at Golang Inc. Call me on 111-888-XXXX
  • 方法是可以繼承和重寫的
  • 存在繼承關系時,按照就近原則,進行調用

結構體和方法補充

因為slice和map這兩種數據類型都包含了指向底層數據的指針,因此在需要復制它們時要特別注意:

type Person struct {
	name   string
	age    int8
	dreams []string
}

func (p *Person) SetDreams(dreams []string) {
	p.dreams = dreams
}

func main() {
	p1 := Person{name: "張三", age: 18}
	data := []string{"吃飯", "睡覺", "打豆豆"}
	fmt.Printf("%p\n",data)		//0xc00006e360
	p1.SetDreams(data) 					//傳的是 data 的內存地址,此時p.dreams和data指向同一塊內存空間
	fmt.Printf("%p\n",p1.dreams)	//0xc00006e360

	// 你真的想要修改 p1.dreams 嗎?
	data[1] = "不睡覺"				//data值的修改會影響person結構體的dream字段
	fmt.Println(p1.dreams)  // [吃飯 不睡覺 打豆豆]
}

正確的做法是在方法中使用傳入的slice的拷貝進行結構體賦值。

func (p *Person) SetDreams(dreams []string) {
	p.dreams = make([]string, len(dreams))
	copy(p.dreams, dreams)
}

同樣的問題也存在於返回值slice和map的情況,在實際編碼過程中一定要注意這個問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM