Go 方法:接收者、方法集与组合的真实边界 | 自在学
Go 方法:接收者、方法集与组合的真实边界
方法看起来只是“把函数名写到变量后面”:counter.Increment()。真正决定代码能否编译、状态会不会被修改、类型能否满足接口的,却是点号前面的接收者。它是值还是指针?能不能取地址?方法属于 T 还是 *T 的方法集?嵌入字段的方法为什么有时会被提升,有时又会因为歧义而消失?
这一页从这些边界出发。我们先把方法声明与普通函数放在一起比较,再拆清值接收者、指针接收者和编译器的隐式转换;随后用方法集解释接口与嵌入,用方法值和方法表达式把方法当成函数传递;最后实现一个位集合,并用包级封装把内部表示保护起来。
先记住一句总纲:方法仍然是函数,接收者仍然是参数。点号语法给调用带来便利,但不会把 Go 变成基于类和继承的语言。
方法声明:给本地类型增加行为
接收者只是一个位置特殊的参数
普通函数把所有参数都写在函数名后面;方法把一个参数挪到函数名前面,这个额外参数叫接收者。下面两个 Distance 做同一件事,但名称所在的命名空间不同:一个是包级函数,另一个是 Point 的方法。
package main
import (
"fmt"
"math"
)
type Point struct {
X , Y float64
}
func Distance ( p , q Point ) float64 {
return math . Hypot ( q . X - p . X , q . Y - p . Y )
}
func ( p Point ) Distance ( q Point ) float64 {
return math . Hypot ( q . X - p . X , q . Y - p . Y )
}
func main () {
p := Point { X : 1 , Y : 2 }
q := Point { X : 4 , Y : 6 }
fmt . Println ( Distance ( p , q )) // 5
fmt . Println ( p . Distance ( q )) // 5
}
p.Distance 是选择器:编译器根据 p 的静态类型和方法名确定目标。结构体字段也使用选择器,例如 p.X。字段和方法共享选择器名称空间,所以 Point 不能同时拥有字段 Distance 和方法 Distance。
接收者名没有 this 或 self 的特殊地位。Go 代码通常用类型名的一两个字母,例如 p Point、c *Counter,并在同一类型的所有方法中保持一致。接收者名会频繁出现,短而稳定比冗长更容易读。
声明-1 表达式 p.Distance(q) 中,编译器主要用什么确定要调用的方法?
A. 只看方法名 Distance B. 接收者 p 的静态类型与方法名 C. q 的运行时类型 D. 方法所在文件名
方法不只属于结构体
接收者的基础类型必须是在当前包定义的类型,而且不能是指针类型或接口类型。它可以以结构体、切片、整数、字符串、映射或函数为底层类型。下面给命名切片 Path 增加总长度方法:
package main
import (
"fmt"
"math"
)
type Point struct { X , Y float64 }
func ( p Point ) Distance ( q Point ) float64 {
return math . Hypot ( q
Point.Distance 与 Path.Distance 没有重载关系,它们只是不同类型上的同名方法。方法不能直接加到其他包定义的类型上,例如当前包不能为 time.Time 新增方法。需要扩展外部类型时,可以定义一个新的本地类型,或把外部类型作为字段组合进自己的结构体。
声明-2 只要底层类型相同,就能在当前包为其他包的命名类型新增方法。
动手题: 为命名类型 type Celsius float64 写一个 Fahrenheit() float64 方法,并打印 20°C 对应的华氏温度。
查看参考实现 package main
import "fmt"
type Celsius float64
func ( c Celsius ) Fahrenheit () float64 {
return float64 ( c ) * 9 / 5 + 32
}
func main () {
fmt . Println
值接收者与指针接收者:先判断语义,再考虑成本
值接收者操作副本
调用值接收者方法时,接收者值会像普通参数一样被复制。下面的 Moved 返回新坐标,不修改原值;这种“输入一个值,得到另一个值”的语义很适合小型、天然不可变的值类型。
package main
import "fmt"
type Point struct { X , Y int }
func ( p Point ) Moved ( dx , dy int ) Point {
p . X += dx
p . Y
不过,“值接收者”不等于“绝对只读”。如果结构体里含有切片、map 或指针,复制的结构体仍可能指向同一份底层数据。值接收者可以借这条引用修改共享对象。接收者类型的选择应该表达整体语义,不能只看方法体里有没有赋值语句。
接收者-1 值接收者方法一定无法改变调用者能观察到的任何数据。
指针接收者操作同一个对象
方法需要修改接收者本身时,应使用 *T 接收者。指针接收者还适合包含 sync.Mutex 等不可复制状态的类型、大型结构体,以及希望所有方法都围绕同一对象身份工作的类型。
package main
import "fmt"
type Counter struct {
name string
n int
}
func ( c * Counter ) Increment () { c . n ++ }
func ( c * Counter ) Reset
这里 counter 是可取地址变量,所以编译器允许用 counter.Increment() 代替 (&counter).Increment()。反方向也成立:若 p 是 *T,调用 T 的值接收者方法时,编译器可以隐式解引用。
工程上可以用这组顺序判断接收者:
方法要修改接收者、类型含锁或其他不可复制状态时,使用指针。
类型很大、复制语义含糊或对象身份重要时,使用指针。
类型很小、自然表示一个值、复制后彼此独立时,值接收者通常更清楚。
同一类型的方法尽量保持一致,避免 API 在 T 与 *T 的方法集之间来回摇摆。
隐式取址有地址边界
编译器的便利有前提:要调用 *T 方法,点号左边必须已经是 *T,或者是能取地址的 T。变量、结构体字段、数组元素和切片元素通常可取地址;map 元素、接口中保存的具体值、函数返回的临时值以及多数复合字面量临时值不能取地址。
package main
type Item struct { N int }
func ( i * Item ) AddOne () { i . N ++ }
func makeItem () Item { return Item { N : 1 } }
map 需要允许扩容和搬迁元素,因此语言不让程序取得 map 元素地址。若业务经常修改 map 中对象,可以改存 map[string]*Item,也可以采用“取出—修改—写回”的值语义。
接收者-3 哪一个表达式可以直接调用 (*Item).AddOne?
A. map[string]Item 的索引结果 B. 返回 Item 的函数调用结果 C. []Item 的切片元素 D. 接口中保存的 Item 具体值
改错题: 下面代码为什么不能编译?给出两种修改方案。
scores := map [ string ] Counter { "Go" : {}}
scores [ "Go" ]. Increment ()
查看原因与修改 scores["Go"] 是不可取地址的 map 元素,无法隐式转换成 *Counter。方案一是让 map 保存指针:
scores := map [ string ] * Counter { "Go" : {}}
scores [ "Go" ]. Increment () 方案二是取出值、修改后写回:
scores := map [ string ] Counter { "Go" : {}}
c := scores [ "Go" ]
c
方法集:能调用不等于属于这个类型
方法集回答的是“这个类型正式拥有哪些方法”。它直接决定类型能否赋给某个接口,也决定哪些方法表达式合法。
对非接口命名类型 T,核心规则是:
最容易混淆的地方是:一个可取地址的 T 变量可以调用 *T 方法,但这不代表该方法进入了 T 的方法集。调用语法额外允许编译器把 value.M() 改写为 (&value).M();接口赋值不会做这种隐式取址。
package main
import "fmt"
type Counter struct { n int }
func ( c Counter ) N () int { return c . n }
func ( c * Counter ) Increment () { c .
方法集-1 已知 (Counter).N 和 (*Counter).Increment,下面哪些赋值可以编译?
预测题: 为什么 var x Incrementer = c 不能靠编译器自动变成 &c?
查看解析 接口变量保存的是赋给它的动态值。若接口赋值自动取地址,var x Incrementer = Counter{} 这类不可取地址值将没有一致语义,而且赋值是否复制或共享状态也会变得隐蔽。因此接口实现严格按类型的方法集判断;需要指针语义时,调用者必须明确传入 &c。
nil 接收者:能进入方法体,才谈得上自定义语义
nil 指针可以成为指针接收者方法的实参。调用时并不会在进入方法前自动解引用;方法体可以先检查接收者,并为 nil 定义含义。链表中把 nil 解释为空表就是一个自然设计:
package main
import "fmt"
type IntList struct {
Value int
Next * IntList
}
// Sum 把 nil *IntList 视为空表。
func ( list * IntList ) Sum () int {
if list == nil {
return 0
}
这里有三条边界:
方法必须是指针接收者;在 nil *T 上调用值接收者方法需要先隐式解引用,会在进入方法体前 panic。
方法体若访问 list.Value 之前没有检查 list == nil,仍会 panic。
无类型的 nil 没有可供选择器解析的静态类型,不能写 nil.Sum();必须先得到 (*IntList)(nil) 或一个已声明的 *IntList 变量。
对 map 这类本身可为 nil 的值也要区分读写:读取 nil map 安全,写入 nil map 会 panic。若方法把 nil map 解释为空集合,文档应明确哪些操作可用,哪些操作要求先初始化。
nil-1 var p *Point = nil;如果 Distance 是 Point 的值接收者方法,执行 p.Distance(q) 会怎样?
A. 自动把 nil 当零值 Point B. 进入 Distance 后由方法自行判断 C. 在隐式解引用接收者时 panic D. 编译错误,因为指针不能调用值方法
设计题: 为二叉树节点定义 Height()。约定 nil 树高度为 0,单节点树高度为 1。
查看参考实现 type Node struct {
Left , Right * Node
}
// Height 返回树高;nil *Node 表示空树,高度为 0。
func ( n * Node ) Height () int {
if n == nil {
return 0
}
left , right := n . Left
结构体嵌入:方法提升是组合,不是继承
嵌入字段与提升
结构体字段只写类型、不写显式字段名时,这个字段称为嵌入字段。字段仍然真实存在,默认字段名是类型名;提升只是让外层值可以用较短的选择器访问内层字段或方法。
package main
import (
"fmt"
"math"
)
type Point struct { X , Y float64 }
func ( p Point ) Distance ( q Point ) float64 {
return math . Hypot ( q
ColoredPoint 并不是 Point 的子类型。要求 Point 参数时,必须传 p.Point,不能直接传 p。调用提升方法时,真正的接收者仍是嵌入字段 p.Point,它不会获得对外层 ColoredPoint 的隐式引用。
嵌入-1 因为 ColoredPoint 提升了 Point 的方法,所以 ColoredPoint 可以直接赋给 Point 变量。
方法集如何随嵌入变化
外层类型的方法集取决于嵌入的是值 T 还是指针 *T:
嵌入 *T 允许多个外层值共享同一内部对象,但零值中的嵌入指针是 nil。直接调用依赖内部字段的提升方法可能 panic,因此构造函数或调用前检查往往更重要。
嵌入-2 S 嵌入值字段 T,T 同时有值方法 Read 和指针方法 Write。哪些结论正确?
浅层优先与同层歧义
选择器按深度解析:先找外层直接声明的字段或方法,再找嵌入一层的成员,然后继续向内。最浅层若只有一个匹配就选它;同一最浅深度出现两个同名成员,选择器就是歧义,必须写出完整路径。
package main
import "fmt"
type FileLogger struct {}
func ( FileLogger ) Log () { fmt . Println ( "写入文件" ) }
type AuditLogger struct {}
func ( AuditLogger ) Log () { fmt .
若 Service 自己声明 Log,它位于深度 0,会遮蔽两个提升的 Log;内层方法依然可以通过完整路径调用。这不是虚方法覆盖,也没有动态派发,只是选择器按静态规则找到最近的成员。
嵌入-3 两个嵌入字段在同一深度都提升了 Log,外层类型未直接声明 Log。调用 s.Log() 的结果是?
A. 按字段声明顺序选择第一个 B. 运行时随机选择 C. 编译期报告选择器歧义 D. 同时调用两个方法
改写题: 缓存同时需要一个互斥锁和一张 map。怎样用嵌入让 cache.Lock() 可用,同时不把它描述成继承?
查看参考设计 var cache = struct {
sync . Mutex
values map [ string ] string
}{
values : make ( map [ string ] string ),
}
func Lookup ( key string ) string {
cache . Lock
方法值与方法表达式:把点号两边拆开
方法值会绑定一个接收者
通常我们在同一行选择并调用方法。只写 p.Distance 而不加调用括号,会得到一个方法值:它已经保存接收者,以后调用时只需要提供剩余参数。
package main
import "fmt"
type Counter struct { n int }
func ( c Counter ) Value () int { return c . n }
func ( c * Counter ) Add ( delta int
方法值在创建时求值并保存接收者。值接收者保存当时的值副本;指针接收者保存指针,之后仍会操作同一个对象。这条差异在回调延迟执行、循环注册处理器和并发代码中尤其重要。
方法值-1 value := c.Value 创建后把 c.n 从 10 改成 20,再调用 value(),结果是什么?
方法表达式把接收者变回第一个参数
方法表达式从类型出发,写作 T.M 或 (*T).M。它不绑定具体对象,而是生成一个普通函数值,并把接收者放回第一个显式参数。
package main
import "fmt"
type Point struct { X , Y int }
func ( p Point ) Add ( q Point ) Point {
return Point { p . X + q . X
方法值适合“固定接收者,稍后调用”,例如把 server.Shutdown 注册成回调。方法表达式适合“固定操作,在许多接收者上执行”,例如根据条件在 Point.Add 与 Point.Sub 之间选择,再遍历一批点。
编程题: 给 Point 增加 Sub,然后写 Transform(path []Point, offset Point, op func(Point, Point) Point),让调用者传 Point.Add 或 Point.Sub。
查看参考实现 func ( p Point ) Sub ( q Point ) Point {
return Point { p . X - q . X , p . Y - q . Y }
}
func Transform ( path
BitSet 实战:让表示、方法与零值一起工作
当集合元素是较小的非负整数,而且需要频繁做并集、交集时,可以用一串 uint64 表示集合。每个 uint64 是一个“字”,包含 64 个位置;整数 x 落在第 x/64 个字的第 x%64 位。
例如 x = 130:
字索引是 130 / 64 = 2;
位索引是 130 % 64 = 2;
掩码是 uint64(1) << 2。
添加元素使用按位或,删除使用按位清除,查询使用按位与;并集可以一次合并 64 个候选元素。
一个可直接运行的实现
package main
import (
"fmt"
"math/bits"
"strconv"
"strings"
)
// BitSet 保存非负整数;零值就是空集合。
type BitSet struct {
words [] uint64
}
func ( s * BitSet ) Has ( x int ) bool {
if
所有方法统一使用指针接收者。Add、Remove、UnionWith 会修改集合;只读方法也保持同一接收者风格,让 API 的方法集一致。代价是 BitSet 值本身没有 String 方法,交给 fmt 时应传 &a。直接写 a.String() 仍能编译,因为局部变量 a 可取地址,但接口匹配和格式化检查按方法集进行。
零值可用是一项很实在的 API 设计:var s BitSet 可以直接 Add,调用者不必先找构造函数。words 在第一次添加元素时按需增长。
位集合-1 元素 144 位于哪个 uint64 字和哪一位?
A. 第 1 个字第 44 位 B. 第 2 个字第 16 位 C. 第 2 个字第 8 位 D. 第 3 个字第 16 位
为什么并集快,复制却要小心
UnionWith 对每个字做一次 |,一次运算同时处理 64 个元素位置。集合密集且上界适中时,这种布局比逐个 map 键操作紧凑;集合极稀疏或整数特别大时,空字会浪费空间,map 或其他稀疏结构可能更合适。
BitSet 内部含切片。直接赋值 copy := original 只复制切片头,两者仍共享底层数组。若 API 提供 Clone,必须复制 words 中的元素:
func ( s * BitSet ) Clone () * BitSet {
if s == nil {
return nil
}
clone := & BitSet { words : make ([] uint64 , len ( s . words ))}
copy
位集合-2 执行 b := a 后,a.words 与 b.words 一定完全独立。
扩展题: 实现 DifferenceWith(other *BitSet),把接收者改为只保留“在自己中、但不在 other 中”的元素。
查看参考实现 func ( s * BitSet ) DifferenceWith ( other * BitSet ) {
limit := len ( s . words )
if len ( other . words ) < limit {
limit = len ( other .
封装:边界在包,不在某个类型内部
Go 用标识符首字母控制可见性:大写开头的名称从包中导出,小写开头的名称只在包内可见。这条规则同时适用于包级名称、结构体字段和方法。没有 private、protected 这样的访问修饰符。
这也说明了封装的单位是包。同一包内的任意函数都可以访问 Account.balance,无论它是不是 Account 的方法;包外代码则不能直接访问这个字段。
package account
import "fmt"
type Account struct {
owner string
balance int64
history [] string
}
func New ( owner string , initial int64 ) ( * Account , error ) {
if owner ==
这里隐藏字段带来三种直接收益:
包外代码无法绕过 Deposit 把金额改成无效状态,方法可以维护不变量。
内部表示可以变化,只要导出的 API 契约不变,调用者不必跟着修改。
排查状态从哪里被修改时,只需检查包内能写这些字段的代码。
History 返回切片副本也很关键。若直接返回 a.history,调用者虽然不能给字段重新赋值,却能通过切片元素修改内部数组,封装就被一条共享引用绕开了。
Go 的 getter 通常不用 Get 前缀:字段概念是 owner,导出读取方法就叫 Owner();对应写方法若确实需要,可以叫 SetOwner()。导出字段并非错误,配置、数据传输对象、坐标点等简单数据常常适合直接暴露。选择时要问:是否存在需要维护的不变量?内部表示以后是否可能变化?一旦字段成为公共 API,再取消导出会破坏兼容性。
API 设计题: 下面哪种类型更适合暴露表示,哪种更适合隐藏表示?说明理由。
本质上就是一串有序坐标、调用者需要字面量构造和 range 的 Path。
当前用 []uint64 实现、未来可能改成稀疏结构的 BitSet。
查看分析 Path 可以定义为 type Path []Point。它的核心语义就是点序列,暴露切片表示能自然获得字面量、索引和遍历能力。
BitSet 更适合定义为含未导出 words 字段的结构体。[]uint64 只是当前实现策略,不是集合的公共语义;隐藏表示后可以改字长、增加元素计数缓存或换用稀疏存储,而不迫使调用者修改代码。
综合练习:把规则放进一套可维护的 API
实现一个 Registry:按名称保存计数器,要求零值可用,Get(name) 只返回当前数值,Increment(name) 能原地更新,Snapshot() 返回与内部状态互不影响的快照。然后回答:map 中应该保存 Counter 还是 *Counter?
综合-1 若 Counter.Increment 使用指针接收者,而 map 保存 Counter 值,则 map 索引结果因为不可 ____,不能直接调用该方法。
查看完整参考实现 package main
import "fmt"
type Counter struct { n int }
func ( c * Counter ) Increment () { c . n ++ }
func ( c * Counter ) N () int {
读方法相关代码时,可以按固定顺序检查:先看接收者是 T 还是 *T,再看点号左边是否可取地址;涉及接口时改看方法集;涉及嵌入时从最浅深度解析选择器;最后检查暴露出去的切片、map 或指针是否绕过了包的封装边界。
.
X
-
p
.
X
,
q
.
Y
-
p
.
Y
)
}
type Path [] Point
func ( path Path ) Distance () float64 {
total := 0.0
for i := 1 ; i < len ( path ); i ++ {
total += path [ i - 1 ]. Distance ( path [ i ])
}
return total
}
func main () {
perimeter := Path {{ 1 , 1 }, { 5 , 1 }, { 5 , 4 }, { 1 , 1 }}
fmt . Println ( perimeter . Distance ()) // 12
}
(
Celsius
(
20
).
Fahrenheit
())
// 68
}
Celsius 是当前包定义的命名类型,所以即使它的底层类型只是 float64,也可以拥有自己的方法。
+=
dy
return p
}
func main () {
original := Point { 1 , 2 }
moved := original . Moved ( 3 , 4 )
fmt . Println ( original ) // {1 2}
fmt . Println ( moved ) // {4 6}
}
()
{
c
.
n
=
0
}
func ( c * Counter ) N () int { return c . n }
func main () {
counter := Counter { name : "下载次数" }
counter . Increment () // 等价于 (&counter).Increment()
counter . Increment ()
fmt . Println ( counter . N ()) // 2
counter . Reset ()
fmt . Println ( counter . N ()) // 0
}
func main () {
item := Item { N : 1 }
item . AddOne () // 可以:item 是可取地址变量
items := [] Item {{ N : 1 }}
items [ 0 ]. AddOne () // 可以:切片元素可取地址
byName := map [ string ] Item { "a" : { N : 1 }}
// byName["a"].AddOne() // 编译错误:map 元素不可取地址
value := byName [ "a" ]
value . AddOne ()
byName [ "a" ] = value // 取出、修改、写回
// makeItem().AddOne() // 编译错误:函数结果是临时值
_ = makeItem
}
.
Increment
()
scores [ "Go" ] = c
n
++
}
type Incrementer interface {
Increment ()
}
type Reader interface {
N () int
}
func main () {
var c Counter
c . Increment () // 可以调用:c 可取地址
var r1 Reader = c // T 的方法集有 N
var r2 Reader = & c // *T 的方法集也有 N
fmt . Println ( r1 . N (), r2 . N ())
// var inc Incrementer = c // 编译错误:Counter 的方法集没有 Increment
var inc Incrementer = & c
inc . Increment ()
fmt . Println ( c . N ()) // 2
}
return list . Value + list . Next . Sum ()
}
func main () {
var empty * IntList
values := & IntList { Value : 3 , Next : & IntList { Value : 5 }}
fmt . Println ( empty . Sum ()) // 0
fmt . Println ( values . Sum ()) // 8
}
.
Height
(),
n
.
Right
.
Height
()
if left > right {
return left + 1
}
return right + 1
}
递归调用 n.Left.Height() 即使 Left 为 nil 也能进入方法体,因为 Height 使用指针接收者并先处理 nil。
.
X
-
p
.
X
,
q
.
Y
-
p
.
Y
)
}
func ( p * Point ) ScaleBy ( factor float64 ) {
p . X *= factor
p . Y *= factor
}
type ColoredPoint struct {
Point
Color string
}
func main () {
p := ColoredPoint { Point : Point { 1 , 1 }, Color : "红" }
q := ColoredPoint { Point : Point { 4 , 5 }, Color : "蓝" }
fmt . Println ( p . X , p . Point . X ) // 1 1
fmt . Println ( p . Distance ( q . Point )) // 5
p . ScaleBy ( 2 )
fmt . Println ( p . Point ) // {2 2}
}
Println
(
"写入审计流"
)
}
type Service struct {
FileLogger
AuditLogger
}
func main () {
var s Service
// s.Log() // 编译错误:两个同深度的 Log 产生歧义
s . FileLogger . Log ()
s . AuditLogger . Log ()
}
()
defer cache . Unlock ()
return cache . values [ key ]
}
匿名结构体含有一个 sync.Mutex 字段,Lock 与 Unlock 从该字段提升。这里是把“锁”和“受保护数据”组合在同一个值里,没有父类或子类关系。复制含锁值会破坏同步语义,因此这类结构应始终通过指针使用,并避免复制。
)
{
c
.
n
+=
delta
}
func main () {
c := Counter { n : 10 }
value := c . Value // 保存值接收者的副本
add := c . Add // 保存指向 c 的指针
c . n = 20
fmt . Println ( value ()) // 10
add ( 5 )
fmt . Println ( c . n ) // 25
}
,
p
.
Y
+
q
.
Y
}
}
func ( p * Point ) Scale ( factor int ) {
p . X *= factor
p . Y *= factor
}
func main () {
add := Point . Add
scale := ( * Point ). Scale
p := Point { 1 , 2 }
q := Point { 3 , 4 }
fmt . Println ( add ( p , q )) // {4 6}
scale ( & p , 3 )
fmt . Println ( p ) // {3 6}
fmt . Printf ( "%T \n " , add ) // func(main.Point, main.Point) main.Point
fmt . Printf ( "%T \n " , scale ) // func(*main.Point, int)
}
[]
Point
,
offset
Point
,
op
func
(
Point
,
Point
)
Point
)
{
for i := range path {
path [ i ] = op ( path [ i ], offset )
}
}
// Transform(path, Point{10, 0}, Point.Add)
// Transform(path, Point{10, 0}, Point.Sub)
方法表达式的类型与 op 完全一致:接收者 Point 成为第一个参数,原参数 q Point 成为第二个参数。
x
<
0
{
return false
}
word , bit := x / 64 , uint ( x % 64 )
return word < len ( s . words ) && s . words [ word ] & ( uint64 ( 1 ) << bit ) != 0
}
func ( s * BitSet ) Add ( x int ) bool {
if x < 0 {
return false
}
word , bit := x / 64 , uint ( x % 64 )
for word >= len ( s . words ) {
s . words = append ( s . words , 0 )
}
before := s . words [ word ]
s . words [ word ] |= uint64 ( 1 ) << bit
return before != s . words [ word ]
}
func ( s * BitSet ) Remove ( x int ) bool {
if x < 0 {
return false
}
word , bit := x / 64 , uint ( x % 64 )
if word >= len ( s . words ) {
return false
}
before := s . words [ word ]
s . words [ word ] &^= uint64 ( 1 ) << bit
return before != s . words [ word ]
}
func ( s * BitSet ) UnionWith ( other * BitSet ) {
for i , word := range other . words {
if i < len ( s . words ) {
s . words [ i ] |= word
} else {
s . words = append ( s . words , word )
}
}
}
func ( s * BitSet ) IntersectWith ( other * BitSet ) {
limit := len ( s . words )
if len ( other . words ) < limit {
limit = len ( other . words )
}
for i := 0 ; i < limit ; i ++ {
s . words [ i ] &= other . words [ i ]
}
for i := limit ; i < len ( s . words ); i ++ {
s . words [ i ] = 0
}
}
func ( s * BitSet ) Len () int {
total := 0
for _ , word := range s . words {
total += bits . OnesCount64 ( word )
}
return total
}
func ( s * BitSet ) Clear () { s . words = nil }
func ( s * BitSet ) Elems () [] int {
result := make ([] int , 0 , s . Len ())
for i , word := range s . words {
for word != 0 {
bit := bits . TrailingZeros64 ( word )
result = append ( result , 64 * i + bit )
word &^= uint64 ( 1 ) << uint ( bit )
}
}
return result
}
func ( s * BitSet ) String () string {
var b strings . Builder
b . WriteByte ( '{' )
for i , x := range s . Elems () {
if i > 0 {
b . WriteByte ( ' ' )
}
b . WriteString ( strconv . Itoa ( x ))
}
b . WriteByte ( '}' )
return b . String ()
}
func main () {
var a , b BitSet
for _ , x := range [] int { 1 , 9 , 144 } {
a . Add ( x )
}
for _ , x := range [] int { 9 , 42 } {
b . Add ( x )
}
fmt . Println ( & a , a . Len ()) // {1 9 144} 3
a . UnionWith ( & b )
fmt . Println ( & a ) // {1 9 42 144}
fmt . Println ( a . Has ( 42 )) // true
a . Remove ( 9 )
fmt . Println ( a . Elems ()) // [1 42 144]
}
(
clone
.
words
,
s
.
words
)
return clone
}
words
)
}
for i := 0 ; i < limit ; i ++ {
s . words [ i ] &^= other . words [ i ]
}
}
&^ 是位清除运算:x &^ y 会把 y 中为 1 的位置从 x 清掉。other 没有覆盖到的高位字无需改变。
""
{
return nil , fmt . Errorf ( "owner cannot be empty" )
}
if initial < 0 {
return nil , fmt . Errorf ( "initial balance cannot be negative" )
}
return & Account { owner : owner , balance : initial }, nil
}
func ( a * Account ) Owner () string { return a . owner }
func ( a * Account ) Balance () int64 { return a . balance }
func ( a * Account ) Deposit ( amount int64 ) error {
if amount <= 0 {
return fmt . Errorf ( "deposit must be positive" )
}
a . balance += amount
a . history = append ( a . history , fmt . Sprintf ( "存入 %d" , amount ))
return nil
}
func ( a * Account ) History () [] string {
return append ([] string ( nil ), a . history ... )
}
return
c
.
n
}
type Registry struct {
counters map [ string ] * Counter
}
func ( r * Registry ) Increment ( name string ) {
if r . counters == nil {
r . counters = make ( map [ string ] * Counter )
}
counter := r . counters [ name ]
if counter == nil {
counter = & Counter {}
r . counters [ name ] = counter
}
counter . Increment ()
}
func ( r * Registry ) Get ( name string ) int {
if r == nil || r . counters [ name ] == nil {
return 0
}
return r . counters [ name ]. N ()
}
func ( r * Registry ) Snapshot () map [ string ] int {
result := make ( map [ string ] int , len ( r . counters ))
for name , counter := range r . counters {
result [ name ] = counter . N ()
}
return result
}
func main () {
var registry Registry
registry . Increment ( "构建" )
registry . Increment ( "构建" )
registry . Increment ( "测试" )
snapshot := registry . Snapshot ()
snapshot [ "构建" ] = 100
fmt . Println ( registry . Get ( "构建" )) // 2
fmt . Println ( snapshot [ "构建" ]) // 100
}
map 保存 *Counter,所以索引结果虽然不可取地址,却已经是指针,可以直接调用指针接收者方法。Registry 在第一次写入时初始化 map,保证零值可用。Snapshot 创建新的 map 并只复制数值,调用者修改快照不会影响内部计数器。