《Go语言圣经》第六章

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.防止使用者肆意地改变对象内的变量。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值