6.1、方法声明
- 函数声明时,在函数名字前面加一个参数,就是这个参数对应类型的方法。
geometry.go
type Point struct {
X, Y float64
}
// Distance 普通的函数
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// Distance Point类型的方法
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
- 附加的参数 p 称为方法的接收者(receiver):用来描述主调方法就像 向对象发送消息。
- 这个附加的变量,相当于其它语言面向对象中定义的 this 或者 self。
- 表达式 p.Distance 称作选择子(selector):为接收者 p 选择合适的 Distance 方法,也用于选择 struct 类型中的某些字段值。
- 方法可以被绑定到任何类型上,只要不是一个指针或者一个 interface。如下的 Path 是一个命名的 slice 类型。
示例一
// Path 是连接多个点的直线段
type Path []Point
// Distance 方法返回路径的长度
func (path Path) Distance() float64 {
sum := 0.0
for i := range path {
sum += path[i-1].Distance(path[i])
}
return sum
}
示例二
perim := Path{
{1, 1},
{5, 1},
{5, 4},
{1, 1},
}
fmt.Println(perim.Distance()) // "12"
- 这两个 Distance 方法拥有不同的类型,编译器会通过方法名和接收者的类型决定调用哪一个函数。
- 在示例一中,path[i-1] 是 Point 类型,因此调用 Point.Distance
- 在示例二中,perim 是 Path 类型,因此调用 Path.Distance
- 使用方法的好处:命名可以比函数更简短。在包的外部进行调用的时候,方法能使用简短的名字并且省略包的名字:
import "gopl.io/ch6/geometry"
perim := geometry.Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}
fmt.Println(geometry.PathDistance(perim)) // "12",独立函数
fmt.Println(perim.Distance()) // "12",geometry.Path的方法
6.2、指针接收者的方法
- 由于函数会复制每一个实参变量,当一个实参太大而我们希望避免复制整个实参时,必须使用指针来传递变量的地址。
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
- 规定:如果 Point 的任何一个方法使用指针接收者,那么所有的 Point 方法都应该使用指针接收者,即时有些方法并不需要。
- 三种合法的方法调用表达式
// Distance Point类型的方法
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
func main() {
q := Point{1, 2}
p := Point{1, 2}
pptr := &p
//下面两个函数效果一样,编译器会自动插入一个隐式的*操作符
pptr.Distance(q)
(*pptr).Distance(q)
//1.实参接收者和形参接收者是同一个类型,比如都是T类型或都是*T类型
Point{1, 2}.Distance(q) // Point
pptr.ScaleBy(2) // *Point
//2.实参接收者是T类型的变量,而形参接收者是*T类型。编译器会隐式地获取变量的地址
p.ScaleBy(2) // 隐式转换为(&p)
//3.实参接收者是*T类型,而形参接收者是T类型。编译器会隐式地解引用接收者,获取实际的取值
pptr.Distance(q) // 隐式转换为(*pptr)
}
- 注意:使用指针类型作为接收器时,如果该方法修改了接收器变量的值,那会改变变量本身真正的值;使用非指针类型作为接收器时,该方法使用的只是该变量的拷贝,对变量本身的值没有影响。
nil 是一个合法的接收者
- 一些函数允许 nil 指针作为实参,方法的接收者也一样,尤其当 nil 是类型中有意义的零值(如 map 和 slice 类型)。
- 当定义一个类型允许 nil 作为接收者时,应显式地标明,如下所示,nil 代表空链表。
// IntList 是整型链表
// *IntList的类型nil代表空链表
type IntList struct {
Value int
Tail *IntList
}
// Sum 返回列表元素的和
func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Tail.Sum()
}
net/url 包中 Values 类型的部分定义源码:
// Values maps a string key to a list of values.
// It is typically used for query parameters and form values.
// Unlike in the http.Header map, the keys in a Values map
// are case-sensitive.
// Values 映射字符串到字符串列表,并且键值会区分大小写
type Values map[string][]string
// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns
// the empty string. To access multiple values, use the map
// directly.
// Get返回第一个具有给定 key 的值,如果不存在,则返回空字符串
func (v Values) Get(key string) string {
if v == nil {
return ""
}
vs := v[key]
if len(vs) == 0 {
return ""
}
return vs[0]
}
// Add adds the value to key. It appends to any existing
// values associated with key.
// Add 添加一个键值到对应 key 列表中
func (v Values) Add(key, value string) {
v[key] = append(v[key], value)
}
urlvalues.go
func main() {
m := url.Values{"lang": {"en"}} // 直接构造
m.Add("item", "1")
m.Add("item", "2")
fmt.Println(m.Get("lang")) // "en"
fmt.Println(m.Get("q")) // ""
fmt.Println(m.Get("item")) // "1" (第一个值)
fmt.Println(m["item"]) // "[1 2]" (直接访问map)
m = nil
fmt.Println(m.Get("item")) // ""
m.Add("item", "3") // panic: assignment to entry in nil map(宕机:赋值给空的map类型)
}
6.3、通过结构体内嵌组成类型
- 当嵌入类型为非指针类型
coloredPoint.go
type Point struct {
X, Y float64
}
type ColoredPoint struct {
Point
Color color.RGBA
}
func (p Point) Distance(q Point) float64 {
dX := q.X - p.X
dY := q.Y - p.Y
return math.Sqrt(dX*dX + dY*dY)
}
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
func main() {
//可以直接使用ColoredPoint内所有的字段而不需要提到Point类型
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"
//同样适用于 Point 类型的方法
red := color.RGBA{R: 255, A: 255}
blue := color.RGBA{B: 255, A: 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
}
- 当嵌入类型为指针类型
// 改写上面的 ColoredPoint,其它不变
type ColoredPoint struct {
*Point
Color color.RGBA
}
func main() {
red := color.RGBA{R: 255, A: 255}
blue := color.RGBA{B: 255, A: 255}
p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point // p 和 q 共享同一个 Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
}
6.4、方法变量与表达式
- 可以把方法(Point.Distance)绑定到一个接收者 p 上。
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法变量
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979",即根号5
scaleP := p.ScaleBy // 方法变量
scaleP(2) // p 变成 (2, 4)
scaleP(3) // 再成 (6, 12)
scaleP(10) // 再成 (60, 120)
- 通过方法变量可以省去匿名函数的使用
// 未使用方法变量
type Rocket struct { ... }
func (r *Rocket) Launch() { ... }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func { r.Launch() })
// 使用方法变量
time.AfterFunc(10 * time.Second, r.Launch)
- 方法变量可以便于调用属于同一个类型的多个方法
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 []Point
func (path Path) TranslateBy(offset Point, add bool) {
var op func(p, q Point) Point
if 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)
}
}
- 方法表达式:
- 调用方法时必须提供接收者,并且按照选择子的语法进行调用
- 格式:写成 T.f 或者 (*T).f,T是类型,并且要把原来方法的接收者换成函数的第一个形参
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // 方法表达式
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(main.Point, main.Point) float64"
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*main.Point, float64)"
6.5、示例:位向量
- Go 语言的集合通常使用 map[Type]bool 来实现
- 在数据流分析领域,集合元素都是小的非负整型,集合拥有许多元素,而且集合的操作多数是求并集和交集,位向量是个理想的数据结构。
- 定义:位向量使用一个无符号整型值的 slice,每一位代表集合中的一个元素。如果设置第 i 位的元素,则认为集合包含 i
intset.go
package main
// 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 >= len(s.words) {
s.words = append(s.words, 0)
}
s.words[word] |= 1 << bit
}
// UnionWith 将会对s和t做并集并将结果存在s中
func (s *IntSet) UnionWith(t *IntSet) {
for i, tword := range t.words {
if i < len(s.words) {
s.words[i] |= tword
} else {
s.words = append(s.words, tword)
}
}
}
- 由于每一个字拥有64位,因此为了定位 x 位的位置,使用商数 x/64 作为字的索引,而 x%64 记作该字内索引。例如:
// 对于数字1,将其加入比特数组:
func (s *IntSet) Add(x int) {
word, bit := x/bitNum, uint(x%bitNum) //0, 1 := 1/64, uint(1%64)
for word >= len(s.words) { // 条件不满足
s.words = append(s.words, 0)
}
s.words[word] |= 1 << bit // s.words[0] |= 1 << 1
}
// 把1存入后,words数组变为了[]uint64{2}
// 同理,假如再将66加入比特数组:
func (s *IntSet) Add(x int) {
word, bit := x/bitNum, uint(x%bitNum) //1, 2 := 66/64, uint(66%64)
for word >= len(s.words) { // 条件满足
s.words = append(s.words, 0) // 此时s.words = []uint64{2, 0}
}
s.words[word] |= 1 << bit // s.words[1] |= 1 << 2
}
// 继续把66存入后,words数组变为了[]uint64{2, 4}
- 对于 words 中的一个元素,要转换为具体的值时:首先取到其位置 i,用 64 * i 作为 已进位数,然后将这个元素转换为二进制数,从右往左数第几位是1则表示相应的有这个值,用这个位数 x + 64*i 就是存入的值
- 相应的,可有如下 String 方法:以字符串输出 IntSet
intset.go
// String 方法以字符串"{1 2 3}"的形式返回集合
func (s *IntSet) String() string {
var buf bytes.Buffer
buf.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()
}
- 例如,前面存入了1和66后,转换过程为:
// []uint64{2 4}
// 对于2: 1 << 1 = 2; 所以 x = 0 * 64 + 1
// 对于4: 1 << 2 = 4; 所以 x = 1 * 64 + 2
// 所以转换为String为{1 66}
- 现在,可以演示 IntSet 了:
func main() {
var x, y IntSet
x.Add(1)
x.Add(144)
x.Add(9)
fmt.Println(x.String()) // "{1 9 144}"
y.Add(9)
y.Add(42)
fmt.Println(y.String()) // "{9 42}"
x.UnionWith(&y)
fmt.Println(x.String()) // "{1 9 42 144}"
fmt.Println(x.Has(9), x.Has(123)) // "true false"
}
6.6、封装
- 首字母大写可从包中导出,首字母小写则不可。
- 封装的单元是包而不是类型
- 封装的优点:
- 1.因为使用方不能直接修改对象的变量,所以不需要更多的语句用来检查变量的值
- 2.隐藏实现细节可以防止使用方依赖的属性发生改变,使得设计者可以更加灵活地改变 API 的实现而不破坏兼容性。
- 3.防止使用者肆意地改变对象内的变量。