方法聲明
type point struct { X, Y float64 }// 普通函數
func Distance(p, q Point) float64 {return math.Hypot(q.x - p.x, q.y - p.Y)
}// Point類型的方法
func (p Point) Distance(q Point) float64 {return math.Hypot(q.x - p.x, q.y - p.Y)
}
方法聲明與普通函數聲明類似,只是在函數名前多一個參數(接收者 ),將方法綁定到對應類型上 。以幾何包為例 ,定義Point
結構體 ,分別展示計算兩點距離的普通函數Distance
和Point
類型的方法Distance
,方法的接收者p
類似面向對象語言中向對象發送消息 ,Go 語言中接收者名字可自行選擇 ,常用類型名首字母 。
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(q)) // 函數調用
fmt.Println(p.Distance(q)) // 方法調用
- 調用方式:調用方法時,接收者在方法名前面 ,如
p.Distance(q)
,與聲明保持一致 ,p.Distance
這種表達式稱作選擇子 。
type Path []Pointfunc(path Path) Distance() float64 {sum := 0.0for i := range path {if i > 0 {sum += path[i-1].Distance(path[i])}}return sum
}
- 命名沖突:不同類型可使用相同方法名 ,如
Point
和Path
類型都有Distance
方法 ,編譯器根據方法名和接收者類型決定調用哪個 。在同一類型命名空間內 ,方法名不能與字段名沖突 。
類型與方法綁定
Go 語言可將方法綁定到多種類型上 ,不僅限于結構體類型 ,像Path
這種命名的 slice 類型也能定義方法 。同一個包下 ,只要類型不是指針類型和接口類型 ,都可聲明方法 。不同類型的同名方法彼此無關 ,如Path.Distance
內部可能使用Point.Distance
計算相鄰點距離 。
指針接收者的方法
func (p *point) ScaleBy(factor float64) {p.x *= factorp.Y *= factor
}
當函數需更新變量,或實參過大想避免復制整個實參時,需用指針傳遞變量地址 。如(*Point).ScaleBy
方法 ,用于按指定因子縮放Point
結構體的坐標 ,其接收者為*Point
指針類型 。
type P *int
func (P) f() { /*...*/ } // 編譯錯誤:非法的接收者類型
- 聲明規則:方法名是
(*Point).ScaleBy
,括號必需 ,否則表達式會被錯誤解析 。習慣上若類型的一個方法使用指針接收者,其他方法也盡量用指針接收者 。命名類型(如Point
)與其指針(*Point
)是不同類型 ,不允許本身是指針的類型進行方法聲明 。
r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"
// or
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p)
// or
p := Point{1, 2}
(&p).ScaleBy(2)
fmt.Println(p)
- 調用規則:可通過
*Point
類型變量調用(*Point).ScaleBy
方法 ,如r := &Point{1, 2}; r.ScaleBy(2)
。若變量是Point
類型,但方法要求*Point
接收者 ,編譯器會對變量進行&p
的隱式轉換 ,只有變量(包括結構體字段、數組或 slice 元素 )允許這種轉換 ,不能對不能取地址的Point
臨時變量調用*Point
方法 。
合法的方法調用表達式需符合以下三種形式:
- 實參接收者和形參接收者是同一類型 ,如
Point{1, 2}.Distance(q)
(都是Point
類型 ),pptr.ScaleBy(2)
(都是*Point
類型 )。 - 實參接收者是
T
類型變量,形參接收者是*T
類型 ,編譯器會隱式獲取變量地址 ,如p.ScaleBy(2)
(p
為Point
類型 )。 - 實參接收者是
*T
類型,形參接收者是T
類型 ,編譯器會隱式解引用接收者獲取實際取值 ,如pptr.Distance(q)
。
復制問題
若類型T
方法接收者是T
本身,調用方法時實參會被復制 ;若接收者是指針類型 ,應避免復制T
實例 ,防止破壞內部數據 ,如bytes.Buffer
實例復制會有問題 。
nil
是一個合法的接收者
nil
在自定義類型方法中的使用type IntList struct {Value intTail *IntList }func (list *IntList) Sum() int {if list == nil {return 0}return list.Value + List.Tail.Sum() }
以整型數鏈表
IntList
為例 ,*IntList
類型中nil
代表空鏈表 。Sum
方法用于返回鏈表元素總和 ,當接收者list
為nil
時 ,直接返回 0 ,否則返回當前節點值與后續鏈表總和 。定義允許nil
作為接收者的類型時 ,應在文檔注釋中明確標明 。
nil
在標準庫類型方法中的使用// Values 映射字符串到字符串列表 type Values map[string][]string// Get 返回第一個具有給定 key 的值 // 如不存在,則返回空字符串 func (v Values) Get(key string) string {if vs := v[key]; 1en(vs) > 0{ return vs[0] } return ""// Add 添加一個鍵值到對應 key 列表中 func (v Values) Add(key, value string){v[key] = append(v[key], value) }
以
net/url
包中的Values
類型為例 ,它本質是映射字符串到字符串列表的map
類型 ,提供Get
和Add
等方法 。Get
方法返回指定鍵的第一個值 ,若鍵不存在或接收者為nil
,返回空字符串 ;Add
方法向對應鍵列表添加值 。當Values
類型接收者為nil
時 ,Get
方法可正常工作 ,但Add
方法會宕機 ,因為嘗試更新一個空map
。方法對接收者引用本身的改變(如設置為nil
或指向不同map
)不會影響調用者 。
通過結構體內嵌組成類型
type Point struct{ X, Y float64 }
type ColordPoint struct {PointColor color.RGBA
}var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X)
cp.Point.Y = 2
fmt.Println(cp.Y)red := color.RGBA{255, 0, 0, 255}
bule := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
q.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
以ColoredPoint
類型為例 ,它嵌套了Point
結構體 ,并包含Color
字段 。通過嵌套 ,ColoredPoint
可直接使用Point
的字段 ,如cp.X
等同于cp.Point.X
,也能調用Point
類型的方法 ,如p.Distance(q.Point)
,實現代碼相當于自動生成了包裝方法來調用Point
聲明的方法 。但要注意ColoredPoint
不是Point
,不能直接傳遞ColoredPoint
實例給要求Point
參數的方法 。
type coloredPoint struct {*PointColor color.RGBA
}
p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
當ColoredPoint
嵌套*Point
指針類型時 ,字段和方法間接地來自所指向的對象 ,如p
和q
可共享一個Point
。結構體類型可擁有多個嵌套字段 ,編譯器處理選擇子(如p.ScaleBy
)時 ,優先查找直接聲明的方法 ,再依次從嵌套字段的方法中查找 。
var (mu sync.Mutexmapping = make(map[string]string)
)func Lookup(key string) string {mu.Lock()v := mapping[key]mu.Unlock()return v
}
var cache = struct {sync.Mutexmapping map[string]string
} {mapping: make(map[string]string),
}func Lookup(key string) string {cache.Lock()v := cache.mapping[key]cache.Unlock()return v
}
結構體嵌套在緩存實現中的應用,最初使用包級別的互斥鎖mu
和mapping
變量保護map
數據 ,后來將相關變量封裝到cache
結構體中 ,該結構體嵌套了sync.Mutex
,這樣cache
變量可直接使用Mutex
的Lock
和Unlock
方法進行加鎖和解鎖操作 ,使代碼結構更清晰 。
方法變量與表達式
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法變量
fmt.Println(ditanceFromP(q))
var origin Point
fmt.Println(distanceFromP(origin))scaleP := p.ScaleBy // 方法變量
scaleP(2) // p -> (2, 4)
scaleP(3) // (6, 12)
scaleP(10) // (60, 120)
- 可將選擇子(如
p.Distance
)賦值給一個變量 ,形成方法變量 ,它是一個函數 ,綁定了方法(Point.Distance
)和接收者p
。調用時只需提供實參 ,無需再提供接收者 。
type Rocket struct { /*...*/ }
func (r *Rocket) Launch { /*...*/ }r := new(Rocket) Launch { /*...*/ }
// time.AfterFunc(10 * time.Second, func() { r. Launch() })
time.AfterFunc(10 * time.Second, r.Launch)
- 在
time.AfterFunc
等場景中 ,方法變量很有用 。如啟動火箭的例子 ,使用方法變量可使代碼更簡潔 ,直接傳遞r.Launch
給time.AfterFunc
,在延遲后調用該方法 。
方法表達式
- 方法表達式寫成
T.f
或(*T).f
形式 ,其中T
是類型 ,它把方法的接收者替換成函數的第一個形參 ,可像普通函數一樣調用 。同樣以Point
結構體的Distance
和ScaleBy
方法為例 ,展示方法表達式的賦值與調用 。
type Point struct{ X, Y float64 }func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }type Path []Pointfunc (path Path) TranslateBy(offset Point, add bool) {var op func(p, q Point) Pointif add {op = Point.Add} else {op = Point.Sub}for i := range path {// 調用 path[i].Add(offset) 或者是 path[i].Sub(offset)path[i] = op(path[i], offset)}
}
當需要用一個值代表同一類型的多個方法 ,并處理不同接收者時 ,方法變量很有幫助 。如Path.TranslateBy
函數 ,根據add
參數決定使用Point.Add
或Point.Sub
方法 ,對路徑上的每個點進行相應計算 。
示例:位向量
在數據分析領域,對于元素為小的非負整型且元素眾多,操作多為求并集和交集的集合,使用map[T]bool
實現集合擴展性雖好但性能欠佳,位向量是更優數據結構 。
// IntSet 是一個包含非負整數的集合
// 零值代表空的集合
type IntSet struct {words []uint64
}
// Has 方法的返回值表示是否存在非負數x
func(s *IntSet) Has(x int) bool{word, bit := x/64, uint(x%64) return word < len(s.words) && s.words[word]&(1<<bit)!=0
}
// Add 添加非負數 x 到集合中
func(s *IntSet) Add(x int){word, bit := x/64, uint(x%64) for word >= 1en(s.words){s.word s = append(s.words, 0)}s.words[word] |= 1<<bit
}// Unionwith 將會對 s 和 t 做并集并將結果存在 s 中
func(s *ntSet) Unionwith(t *IntSet){for i, tword := range t.words { if i < len(s.words){s.words[1] |= tword} else { s.words = append(s.words, tword)}
}// String方法以字符串"{1 2 3}"的形式返回集中
func (s *IntSet) String() string {var buf bytes.Bufferbuf.WriteByte('{')for i, word := range s.words {if word == 0 {continue}for j := 0; j < 64; j++ {if word&(1<<uint(j)) != 0 {if buf.Len() > len("{") {buf.WriteByte(' ')}fmt.Fprintf(&buf, "%d", 64*i+j)}}}buf.WriteByte('}')return buf.String()
}
IntSet
類型的實現與方法
- 結構定義:
IntSet
結構體包含words []uint64
字段 ,用無符號整型值的 slice 表示集合 ,每一位代表集合中的一個元素 。 - 方法功能:
Has
方法:判斷集合中是否存在非負數x
,通過計算x
所在的字索引和位索引 ,檢查對應位是否為 1 。Add
方法:向集合中添加非負數x
,確定x
所在字索引 ,若字不存在則擴展words
,然后將對應位置為 1 。UnionWith
方法:對兩個IntSet
求并集 ,遍歷另一個集合的字 ,與當前集合對應字按位或操作 ,不存在的字添加到當前集合 。String
方法:以字符串形式輸出集合元素 ,使用bytes.Buffer
,遍歷words
,對每個字的每一位檢查 ,是 1 則將對應元素添加到字符串 。
IntSet
類型方法聲明為指針類型接收者 ,使用值調用方法時需注意 ,編譯器會隱式插入&
操作符獲取指針以調用String
方法 ,若無String
方法 ,fmt.Println
會直接輸出結構體 。
封裝
- 概念:封裝(數據隱藏 )是面向對象編程重要方面,指變量或方法不能通過對象訪問 。
- 實現方式:Go 語言通過標識符首字母大小寫控制命名可見性 ,首字母大寫可從包中導出 ,首字母小寫則不導出 ,要封裝對象需使用結構體 。以
IntSet
類型為例 ,若定義為結構體且字段words
首字母小寫 ,則該字段在包外不可見 ;若重新定義IntSet
為 slice 類型 ,表達式*s
可在其他包內使用 ,但會暴露內部表示 。Go 語言中封裝單元是包而非類型 ,結構體字段在同包內代碼可見 。
優點
- 減少變量檢查:使用方不能直接修改對象變量 ,減少檢查變量值的代碼 。
type Buffer struct {buf []byteinitial [64]byte/*... */
}// Grow方法按需擴展緩沖區的大小
// 保證n個字節的空間
func (b *Buffer) Grow(n int) {if b.buf == nil {b.buf = b.initial[:0] // 最初使用預分配的空間}if len(b.buf)+n > cap(b.buf) {buf := make([]byte, b.Len(), 2*cap(b.buf)+n)copy(buf, b.buf)b.buf = buf}
}
- 隱藏實現細節:防止使用方依賴的屬性改變 ,方便設計者靈活改變 API 實現且不破壞兼容性 。以
bytes.Buffer
為例 ,其內部字段未導出 ,外部使用者無需關心實現細節 ,僅感知性能提升 。
type Counter struct { n int }func (c *Counter) N() int { return c.n }
func (c *Counter) Increment() { c.n++ }
func (c *Counter) Reset() { c.n = 0 }
- 保護對象內部資源:防止使用者隨意改變對象內變量 ,包作者可通過包內函數維護對象內部資源 。如
Counter
類型 ,使用者只能通過特定方法遞增或重置計數器 ,不能隨意設置計數值 。
導出字段與封裝的權衡
Go 語言允許導出字段 ,但需慎重考慮 API 兼容性、維護復雜度等因素 。同時封裝并非總是必需 ,如time.Duration
類型暴露int64
整型用于運算 。
參考資料:《Go程序設計語言》