方法
在面向對象編程的編程思想里,類、對象、方法是基礎。類比到Golang中
// 類
type Point struct {X, Y int}
// 對象
p := Point{1, 2}
// 方法 即綁定在struct上的函數
// ...
方法聲明
方法和函數類似,區別在於它在函數名前多了一個參數(接收器),用來將方法綁定在參數對應的類型上
package main
import (
"fmt"
"math"
)
type Point struct {
X, Y float64
}
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func main() {
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(p.Distance(q)) // 5
}
每個類型都有自己的命令空間,在同一個命名空間里不能有相同名稱的方法和成員
type Line struct {
Start Point
End Point
// Length float64
// 如果取消上面這行的注釋 編譯報錯:type Line has both field and method named Length
}
func (L Line) Length() float64 {
return L.Start.Distance(L.End)
}
func main() {
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(p.Distance(q)) // 5
line := Line{p, q}
fmt.Println(line.Length()) // 5
}
不同類型的命名空間是獨立的,可以在不同類型中使用相同名字的方法
type Path []Point
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}
func main() {
perim := Path{
{1, 1},
{5, 1},
{5, 4},
{1, 1},
}
fmt.Println(perim.Distance()) // 12
}
指針接收者的方法
函數調用實參變量是以復制一份的方式傳遞的,如果我們想在函數中進行更改會很麻煩;如果一個實參太大,我們希望避免復制整個實參,我們可以通過指針的方式傳遞變量地址。這也同樣使用與方法
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
func main() {
p := Point{1, 2}
p.ScaleBy(200)
fmt.Printf("%+v", p) // {X:200 Y:400}
}
習慣上,如果Point
上任何一個方法綁定指針接收者,那么所有的Point方法都應該使用指針接收者。方法的接收者只能是類型(Point)或者類型指針(Point)。
為了防止混淆,不允許本身是指針的類型進行方法聲明:
type p *int
func (p) f() {/*...*/} // 編譯錯誤:非法的接收者類型
以下幾種寫法都是合法的:
// case1
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // {2, 4}
// case2
p1 := Point{1, 2}
pptr := &p1
pptr.ScaleBy(2)
fmt.Println(p1) // {2, 4}
// case3
p2 := Point{1, 2}
(&p2).ScaleBy(2)
fmt.Println(p2) // {2, 4}
注意,不能對一個不能取地址的Point接收者參數調用*Point方法,因為無法獲得臨時變量的地址。
Piont{1,2}.ScaleBy(2) // 編譯錯誤
反過來,指針類型(*Point)變量,它是可以調用Point類型的方法
type Point struct{}
func (p *Point) PtrFunc() {}
func (p Point) Func() {}
func main() {
p := Point{}
ptr := &Point{}
ptr.PtrFunc()
ptr.Func()
Point{}.Func()
Point{}.PtrFunc() // 編譯錯誤:cannot call pointer method on Point literal
p.Func()
p.PtrFunc() // 編譯器做了隱式轉換
}
疑惑:如果所有類型T方法的接收者是T自己(而非*T),那么復制它的實例是安全的;調用方法的時候必須進行一次復制。但是任何方法的接收者是指針的情況下,應該避免復制T的實例,因為這么做可能會破壞原本的數據。
nil是一個合法的接收
方法的接收者可以是nil
// *IntList的類型nil代表空列表
type IntList struct {
Value int
Next *IntList
}
func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Next.Sum()
}
func main() {
a1 := IntList{1, nil}
a2 := IntList{2, &a1}
a3 := IntList{3, &a2}
fmt.Println(a3.Sum()) // 6
}
當定義一個類型允許為nil作為接收者,應該在文檔中顯式地表明
通過結構體內嵌組成類型
在一個結構體A中嵌套另一個結構體B,則結構體A可以調用結構體B的方法
import (
"fmt"
"image/color"
"math"
)
type Point struct{ X, Y float64 }
func (p Point) Distance(q Point) float64 {
return math.Hypot(p.X-q.X, p.Y-q.Y)
}
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
type ColoredPoint struct {
Point
Color color.RGBA
}
func main() {
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // 1
p := ColoredPoint{Point{1, 1}, color.RGBA{255, 0, 0, 255}}
q := ColoredPoint{Point{5, 4}, color.RGBA{0, 0, 255, 255}}
//fmt.Println(p.Distance(q)) // 編譯錯誤:cannot use q (type ColoredPoint) as type Point in argument to p.Point.Distance
fmt.Println(p.Distance(q.Point)) // 5
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // 10
}
ColoredPoint
類型內嵌了Point
類型,它可以調用Point
的Distance
和ScaleBy
方法。也可以直接訪問Point
的成員變量。
實際上,內嵌字段會告訴編譯生成額外的包裝方法來調用 Point
聲明的方法:
func (p ColoredPoint) Distance(q Point) float64 {
return p.Point.Distance(q)
}
func (p *ColoredPoint) ScaleBy(factor float64) {
p.Point.ScaleBy(factor)
}
匿名字段可以是指向命名類型的指針,字段和方法間接地來自於所指向的對象。這可以讓我們共享通用的結構以及使對象之間的關系更加動態、多樣化。
我們將ColoredPoint
的匿名字段改成指針類型,在對比一下和上面非指針類型的區別:
import (
"fmt"
"image/color"
"math"
)
type Point struct{ X, Y float64 }
func (p Point) Distance(q Point) float64 {
return math.Hypot(p.X-q.X, p.Y-q.Y)
}
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
type ColoredPoint struct {
*Point
Color color.RGBA
}
func main() {
var cp ColoredPoint
cp.Point = &Point{} // 匿名指針類型的默認值是nil,必須對其進行初始化
cp.Point.X = 1 // 如果沒有上面的那一行,執行報錯:panic: runtime error: invalid memory address or nil pointer dereference
fmt.Println(cp.Point.X) // 1
p := ColoredPoint{&Point{1, 1}, color.RGBA{255, 0, 0, 255}} // 初始化是Point傳地址
q := ColoredPoint{&Point{5, 4}, color.RGBA{0, 0, 255, 255}}
//fmt.Println(p.Distance(q)) // 編譯錯誤:cannot use q (type ColoredPoint) as type Point in argument to p.Point.Distance
fmt.Println(p.Distance(*q.Point)) // 5 實參傳遞時,要轉化為值
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(*q.Point)) // 10
}
結構體類型也可以由多個匿名字段
type ColoredPoint struct {
Point
color.RGBA
}
p := ColoredPoint{Point{1, 1}, color.RGBA{255, 0, 0, 255}}
當調用p.ScaleBy
方法時,它會先查找ColoredPoint
有沒有聲明這個方法,如果沒有,再從其內嵌對象Point
和color.RGBA
上查找,再從Point
和color.RGBA
的內嵌對象上查找。當同一個查找級別中有同名方式時,編譯器報錯;
type A struct {}
func (a A) Func() {}
type B struct {}
func (b B) Func() {}
type C struct {
A
B
}
func main() {
c := C{}
c.Func() // 編譯錯誤:ambiguous selector c.Func
}
方法只能在命名的類型(比如Point)和指向他們指針(*Point)中聲明,但內嵌幫助我們能夠在未命名的結構體類型中聲明方法。
方法變量與表達式
我們可以將方法賦予一個方法變量,方法變量是一個函數,本質上會綁定到接收者上,可以理解為方法的引用,方法變量只要傳遞實參就可以調用成功。
a := Point{1, 2}
b := Point{4, 6}
distanceFromA := a.Distance // 方法變量
fmt.Println(distanceFromA(b)) // 5
origin := Point{0, 0}
fmt.Println(distanceFromA(origin)) // 2.23606797749979
scaleA := a.ScaleBy // 方法變量
scaleA(2)
fmt.Println(a) // {2, 4}
方法表達式與方法變量相似,區別是方法變量是由將類型聲明的變量的方法賦予的,而方法表達式是有類型的方法賦予的,有點繞,看一下例子:
a := Point{1, 2}
b := Point{4, 6}
distanceFromA := a.Distance // 方法變量 由a的方法賦予
distance := Point.Distance // 方法表達式 由Point類型的方法賦予
方法的接收者會替換成函數的第一個參數
fmt.Println(distanceFromA(b)) // 5 方法變量
fmt.Println(distance(a, b)) // 5 方法表達式
fmt.Printf("%T\n", distance) // func(Point, Point) float64
// scale := Point.ScaleBy // 編譯報錯:nvalid method expression Point.ScaleBy (needs pointer receiver: (*Point).ScaleBy
scale := (*Point).ScaleBy
scale(&a, 2)
fmt.Println(a)
fmt.Printf("%T\n", scale) // func(*Point, float64)
封裝
控制變量和方法不能通過對象訪問(私有),即為封裝。Go語言中通過控制命名的大小寫來實現,首字母大寫的標識符可以被導出,小寫的就不可以。因此,可以通過結構體來是實現封裝,向調用者隱藏重要的數據和實現細節,防止非法更改。
type IntSet struct {
words []uint64
}
type IntSet2 []uint64
對比兩個類型,IntSet
將實際存儲數據的slice封裝成了一個不可訪問字段,IntSet2
也將數據存儲在slice,但它是可以被訪問的,我們可以同*s在其他包中訪問、更改。
思考:結構體里的字段一定都要封裝起來,不讓使用者看到嗎?
封裝的優點:
- Go語言封裝的單元是包而不是類型,包內的函數和方法對結構體的字段是可見的
- 實現細節可以對包的使用方屏蔽,方便設計者靈活改變
- 防止使用者非法更改結構體內的變量
封裝的缺點:
- 需要設計者編寫很多的方法來實現對字段的讀取和更新,因為調用者無法自助。
封裝並不總會需要的,要結合實際的適用場景區別對待。