Golang通脈之方法
方法和接收者
Go語言中的方法(Method)
是一種作用於特定型別變數的函式。這種特定型別變數叫做接收者(Receiver)
。接收者的概念就類似於其他語言中的this
或者 self
。
Go 語言中同時有函式和方法。一個方法就是一個包含了接受者的函式,接受者可以是命名型別或者結構體型別的一個值或者是一個指標。所有給定型別的方法屬於該型別的方法集
方法只是一個函式,它帶有一個特殊的接收器型別,它是在func
關鍵字和方法名之間編寫的。接收器可以是struct
型別或非struct
型別。接收方可以在方法內部訪問。
方法能給使用者自定義的型別新增新的行為。它和函式的區別在於方法有一個接收者,給一個函式新增一個接收者,那麼它就變成了方法。接收者可以是值接收者,也可以是指標接收者
在呼叫方法的時候,值型別既可以呼叫值接收者的方法,也可以呼叫指標接收者的方法;指標型別既可以呼叫指標接收者的方法,也可以呼叫值接收者的方法。
也就是說,不管方法的接收者是什麼型別,該型別的值和指標都可以呼叫,不必嚴格符合接收者的型別。
方法的定義格式如下:
func (t Type) methodName(parameter)(return) {
}
其中,
t
:接收者中的引數變數名在命名時,官方建議使用接收者型別名稱首字母的小寫,而不是self
、this
之類的命名。Type
:接收者型別和引數類似,可以是指標型別和非指標型別。methodName
、parameter
、return
//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
}
什麼時候應該使用指標型別接收者
- 需要修改接收者中的值
- 接收者是拷貝代價比較大的大物件
- 保證一致性,如果有某個方法使用了指標接收者,那麼其他的方法也應該使用指標接收者。
方法和函式
已經有了函式,為什麼還要使用方法?
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.。
為什麼可以用函式來寫相同的程式呢?有以下幾個原因:
- Go不是一種純粹面向物件的程式語言,它不支援類。因此,型別的方法是一種實現類似於類的行為的方法。
- 相同名稱的方法可以在不同的型別上定義,而具有相同名稱的函式是不允許的。
任意型別新增方法
在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的情況,在實際編碼過程中一定要注意這個問題。