方法声明
            
            
              go
              
              
            
          
          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 语言中接收者名字可自行选择 ,常用类型名首字母 。
            
            
              go
              
              
            
          
          p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(q))   // 函数调用
fmt.Println(p.Distance(q)) // 方法调用
        - 调用方式 :调用方法时,接收者在方法名前面 ,如
p.Distance(q),与声明保持一致 ,p.Distance这种表达式称作选择子 。 
            
            
              go
              
              
            
          
          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
}
        - 命名冲突 :不同类型可使用相同方法名 ,如
Point和Path类型都有Distance方法 ,编译器根据方法名和接收者类型决定调用哪个 。在同一类型命名空间内 ,方法名不能与字段名冲突 。 
类型与方法绑定
Go 语言可将方法绑定到多种类型上 ,不仅限于结构体类型 ,像Path这种命名的 slice 类型也能定义方法 。同一个包下 ,只要类型不是指针类型和接口类型 ,都可声明方法 。不同类型的同名方法彼此无关 ,如Path.Distance内部可能使用Point.Distance计算相邻点距离 。
指针接收者的方法
            
            
              go
              
              
            
          
          func (p *point) ScaleBy(factor float64) {
    p.x *= factor
    p.Y *= factor
}
        当函数需更新变量,或实参过大想避免复制整个实参时,需用指针传递变量地址 。如(*Point).ScaleBy方法 ,用于按指定因子缩放Point结构体的坐标 ,其接收者为*Point指针类型 。
            
            
              go
              
              
            
          
          type P *int
func (P) f() { /*...*/ } // 编译错误:非法的接收者类型
        - 声明规则 :方法名是
(*Point).ScaleBy,括号必需 ,否则表达式会被错误解析 。习惯上若类型的一个方法使用指针接收者,其他方法也尽量用指针接收者 。命名类型(如Point)与其指针(*Point)是不同类型 ,不允许本身是指针的类型进行方法声明 。 
            
            
              go
              
              
            
          
          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在自定义类型方法中的使用
gotype IntList struct { Value int Tail *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在标准库类型方法中的使用
go// 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)不会影响调用者 。
通过结构体内嵌组成类型
            
            
              go
              
              
            
          
          
        以ColoredPoint类型为例 ,它嵌套了Point结构体 ,并包含Color字段 。通过嵌套 ,ColoredPoint可直接使用Point的字段 ,如cp.X等同于cp.Point.X ,也能调用Point类型的方法 ,如p.Distance(q.Point) ,实现代码相当于自动生成了包装方法来调用Point声明的方法 。但要注意ColoredPoint不是Point ,不能直接传递ColoredPoint实例给要求Point参数的方法 。
嵌套类型的方法调用
当ColoredPoint嵌套*Point指针类型时 ,字段和方法间接地来自所指向的对象 ,如p和q可共享一个Point 。结构体类型可拥有多个嵌套字段 ,编译器处理选择子(如p.ScaleBy )时 ,优先查找直接声明的方法 ,再依次从嵌套字段的方法中查找 。
结构体嵌套在缓存实现中的应用
通过展示简单缓存实现的例子 ,说明结构体嵌套的实用性 。最初使用包级别的互斥锁mu和mapping变量保护map数据 ,后来将相关变量封装到cache结构体中 ,该结构体嵌套了sync.Mutex ,这样cache变量可直接使用Mutex的Lock和Unlock方法进行加锁和解锁操作 ,使代码结构更清晰 。
方法变量与表达式
方法变量
- 可将选择子(如
p.Distance)赋值给一个变量 ,形成方法变量 ,它是一个函数 ,绑定了方法(Point.Distance)和接收者p。调用时只需提供实参 ,无需再提供接收者 。通过Point结构体的Distance和ScaleBy方法示例 ,展示方法变量的赋值与调用 。 - 在
time.AfterFunc等场景中 ,方法变量很有用 。如启动火箭的例子 ,使用方法变量可使代码更简洁 ,直接传递r.Launch给time.AfterFunc,在延迟后调用该方法 。 
方法表达式
- 方法表达式写成
T.f或(*T).f形式 ,其中T是类型 ,它把方法的接收者替换成函数的第一个形参 ,可像普通函数一样调用 。同样以Point结构体的Distance和ScaleBy方法为例 ,展示方法表达式的赋值与调用 。 
方法变量的应用场景
当需要用一个值代表同一类型的多个方法 ,并处理不同接收者时 ,方法变量很有帮助 。如Path.TranslateBy函数 ,根据add参数决定使用Point.Add或Point.Sub方法 ,对路径上的每个点进行相应计算 。
示例:位向量
在数据分析领域,对于元素为小的非负整型且元素众多,操作多为求并集和交集的集合,使用map[T]bool实现集合扩展性虽好但性能欠佳,位向量是更优数据结构 。
            
            
              go
              
              
            
          
          // 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)
         }
}
        IntSet类型的实现与方法
- 结构定义 :
IntSet结构体包含words []uint64字段 ,用无符号整型值的 slice 表示集合 ,每一位代表集合中的一个元素 。 - 方法功能
Has方法 :判断集合中是否存在非负数x,通过计算x所在的字索引和位索引 ,检查对应位是否为 1 。Add方法 :向集合中添加非负数x,确定x所在字索引 ,若字不存在则扩展words,然后将对应位置为 1 。UnionWith方法 :对两个IntSet求并集 ,遍历另一个集合的字 ,与当前集合对应字按位或操作 ,不存在的字添加到当前集合 。String方法 :以字符串形式输出集合元素 ,使用bytes.Buffer,遍历words,对每个字的每一位检查 ,是 1 则将对应元素添加到字符串 。
 
注意事项
通过示例展示IntSet类型的使用 ,包括添加元素、求并集、判断元素是否存在等操作 。强调IntSet类型方法声明为指针类型接收者 ,使用值调用方法时需注意 ,编译器会隐式插入&操作符获取指针以调用String方法 ,若无String方法 ,fmt.Println会直接输出结构体 。
封装
- 概念:封装(数据隐藏 )是面向对象编程重要方面,指变量或方法不能通过对象访问 。
 - 实现方式 :Go 语言通过标识符首字母大小写控制命名可见性 ,首字母大写可从包中导出 ,首字母小写则不导出 ,要封装对象需使用结构体 。以
IntSet类型为例 ,若定义为结构体且字段words首字母小写 ,则该字段在包外不可见 ;若重新定义IntSet为 slice 类型 ,表达式*s可在其他包内使用 ,但会暴露内部表示 。强调 Go 语言中封装单元是包而非类型 ,结构体字段在同包内代码可见 。 
优点
- 减少变量检查:使用方不能直接修改对象变量 ,减少检查变量值的代码 。
 - 隐藏实现细节 :防止使用方依赖的属性改变 ,方便设计者灵活改变 API 实现且不破坏兼容性 。以
bytes.Buffer为例 ,其内部字段未导出 ,外部使用者无需关心实现细节 ,仅感知性能提升 。 - 保护对象内部资源 :防止使用者随意改变对象内变量 ,包作者可通过包内函数维护对象内部资源 。如
Counter类型 ,使用者只能通过特定方法递增或重置计数器 ,不能随意设置计数值 。介绍了用于获取或修改内部变量的getter和setter函数 ,命名时getter常省略Get前缀 。 
导出字段与封装的权衡
Go 语言允许导出字段 ,但需慎重考虑 API 兼容性、维护复杂度等因素 。同时指出封装并非总是必需 ,如time.Duration类型暴露int64整型用于运算 ;对比IntSet和geometry.Path ,Path定义为 slice 类型可方便使用 slice 语法 ,IntSet不对外透明有其合理性 。
etter函数 ,命名时getter常省略Get`前缀 。
导出字段与封装的权衡
Go 语言允许导出字段 ,但需慎重考虑 API 兼容性、维护复杂度等因素 。同时指出封装并非总是必需 ,如time.Duration类型暴露int64整型用于运算 ;对比IntSet和geometry.Path ,Path定义为 slice 类型可方便使用 slice 语法 ,IntSet不对外透明有其合理性 。
参考资料:《Go程序设计语言》