golang学习随便记7

Posted sjg20010414

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang学习随便记7相关的知识,希望对你有一定的参考价值。

方法

golang 自认为它是支持 OOP 的,但它的 OOP 和 C++、Java、C#、php、Python等都不太一样,所以,它是用自己的方式支持了 OOP 思想。

方法声明

因为 golang 并没有“类”的概念,所以,golang方法是从“外部”附加到类型上的。声明方法时,差不多就是一个函数,只是函数名字前多一个特定类型的参数,这个参数表示要把方法绑定到这个参数对应的类型上。这个附加的参数,也称为“方法的接收者",这是对于主调函数来说的。

下面的例子,第一个 Distance 函数表示计算2点间距离,第二个 Distance 函数则是一个方法,表示 Point类型拥有一个 Distance 方法,计算它的实例点到另一点的距离。

package main

import (
	"fmt"
	"math"
)

func main() {
	p := Point{1, 2}
	q := Point{3, 5}
	fmt.Println(Distance(p, q))         // p,q 之间的距离
	fmt.Println(p.Distance(q))          // 从 p 出发到 q 的距离
	fmt.Println(q.Distance(p))          // 从 q 出发到 p 的距离
}

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)
}

在上面的方法声明中,方法内直接用参数 p 表示运行时的那个实例(其实是副本,golang 没有 C++ 那样的 this 指针 或 Python那样的 self 表示,也就是声明时指代实例没有固定名称。

p.Distance 、p.X、p.Y 之类叫 selector,选择方法名时不能和字段名冲突,但不同的类型可以有相同的方法名 (这也是合理的),即方法名相同接收者不同。golang 并没有 C++ 那样的方法重载,所以,即使参数类型或数量不同,也不允许两个方法同名,即 selector必须唯一

指针接收者的方法

在前面的例子中,对于主调函数main来说,它会把点 p 拷贝赋值到方法接收者参数。换句话说,此时 Distance 内部无论如何操作,不会修改原来点 p 的数据。另外,但 p 的类型是一个庞大的结构时,这种拷贝也会带来大的开销。所以,我们需要”指针接收者“。

package main

import (
	"fmt"
)

func main() {
	p := Point{0, 0}
	q := Point{0, 0}
	p.MoveLeft0(3)
	fmt.Println(p)
	q_ptr := &q
	q_ptr.MoveLeft(3)
	fmt.Println(q)
	(&q).MoveLeft(3)
	fmt.Println(q)
	q.MoveLeft(3)
	fmt.Println(q)
	(&Point{0, 0}).MoveLeft(3)
	//Point{0, 0}.MoveLeft(3)			// compile error
}

type Point struct{ X, Y float64 }

func (p Point) MoveLeft0(dx float64) {
	p.X += dx
}

func (p *Point) MoveLeft(dx float64) {
	p.X += dx
}

输出

{1 2}
{5 5}

这里有几点需要注意

  1. 无论是指针接收者还是非指针接收者,内部都是”点语法”,并不存在C/C++/PHP等->的用法
  2. 如果一个类型本身是指针类型,那么不可能再出现针对它的指针接收者(编译器不允许,不然上面的第1点会出问题)
  3. 真实程序中,即使方法不需要用指针接收者(例如只读操作的方法),多数时候遵循都用指针接收者
  4. 使用指针接收者的方法时,可以不显式进行取地址操作 (例子中 q.MoveLeft(3) 和 (&q).MoveLeft(3) 等价),因为编译器会帮你隐式转换,但必须确保被转换的是可以取地址操作的变量。即形参是 *T 类型,实参接收者是 T类型的变量,编译器隐式转换。反之,形参是 T 类型,实参接收者是 *T类型,编译器会隐式解引用接收者(就像第1点那样)。
  5. nil 是可以作为接收者的,特别是 nil 在一些类型中是有意义的零值(map和slice的零值为nil)

结构体嵌套来组合字段和方法

golang的 OOP 并没有继承,它使用接口和组合来实现复用。通过结构体的嵌套,既可以实现字段的组合,又可以实现方法的组合。

package main

import (
	"fmt"
	"image/color"
	"math"
)

func main() {
	red := color.RGBA{255, 0, 0, 255}
	blue := color.RGBA{0, 0, 255, 255}
	p := ColoredPoint{Point{2, 1}, red}
	q := ColoredPoint{Point{6, 4}, blue}
	p.MoveLeft(1)
	fmt.Println(p)				// {{1 1} {255 0 0 255}}
	fmt.Println(q.Point.Y)		// 4
	fmt.Println(q.Y)			// 4
	q.X = 5
	fmt.Println(q)				// {{5 4} {0 0 255 255}}
	fmt.Println(p.Distance(q.Point)) // 5
}

type Point struct{ X, Y float64 }

func (p *Point) MoveLeft(dx float64) {
	p.X -= dx
}

func (p *Point) Distance(q Point) float64 {
	return math.Hypot(p.X-q.X, p.Y-q.Y)
}

type ColoredPoint struct {
	Point
	Color color.RGBA
}

在上面的组合中,ColoredPoint 内部的 Point,含义是 Point  Point (即它的类型是 Point,当作为 ColoredPoint 的成员需要引用时,它的成员名也是 Point,我们在 ES6 的JS中能看到有点类似的做法,JS里面对象的键名和函数名就是同一个 ),可以理解为嵌入类型就是嵌入成员

在上面的组合中,ColoredPoint 组合了 Point,Point的成员将自动成为ColoredPoint的成员,Point的方法自然也成了ColoredPoint的方法,golang支持用快捷语法使用这些成员或者方法。从这一点看,golang结构体嵌套结构体,实现了类似trait的“注入”功能,达到了代码复用目的。但是和trait不同的是,golang嵌入的结构体的方法,是不能访问外层结构体的成员的,所以,这是一种静态的复用(相当于编译器生成了同样名字的方法,而接收者是外层结构体)。

ColoredPoint的成员 Color,也是一个结构体成员,那为什么没有直接嵌入 RGBA (不写Color color.RGBA而直接写 color.RGBA并不会报错)?个人认为,只有当认为包含被嵌入类型所有成员是符合直觉逻辑和业务逻辑时,才会做整体嵌入,如果只需要一个类型的小部分成员,则不适合整体嵌入,还是单独一个成员变量比较好。另外,如果整体嵌入会丢失它自己的完整性,也不合理时不该整体嵌入,例如,直接用 color.RGBA,意味着 R、G、B、A 都是 ColoredPoint 的成员,这不符合我们对“有颜色的点”的逻辑理解(颜色对有颜色的点应该是一个整体概念)。

ColoredPoint 并不是 Point 的继承类型,两者类型不同,这和带继承的 OOP 语言不同,那里子类是父类的兼容类型,而这里 ColoredPoint 并非 Point 的继承类型,所以,尽管我们调用Distance方法时,前面是 p.Distance(...),但参数必须是 Point 类型的,只能写 q.Point,而不能直接写 q。

像上面代码中Point那样的匿名字段类型可以是指向命名类型的指针*Point,此时,字段和方法将间接来自于所指向的对象。匿名字段类型用指针可以实现共享内存,对象之间的关系也可以更动态和灵活(也更复杂)。

如果一个方法的名字既在外层结构体出现,也在内层结构体出现,使用快捷语法访问字段的时候,必然存在一个查找顺序问题。golang是先从直接声明的位置开始查找,然后往里查找,同一层查找时,必须确保只能找到一个同名方法,不然编译器报告selector不明确的错误(即嵌套结构体时最好不要在同层定义同名方法,如果出现这样的情况,只能明确指明消除冲突)。

方法变量与表达式

方法变量的概念和函数变量是差不多的

p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance
fmt.Println(distaceFromP(q))

如果某个API的参数是函数值(回调函数),并且希望这个函数是特定接收者的方法,那么使用方法变量比用匿名函数打包一层要简洁得多

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)        // 方法变量作为回调,更简洁

在上面的代码中,我们知道方法变量是绑定到某个类型的实例上的 (distanceFromP 绑定到实例p, r.Launch 绑定到 r)。而对于方法表达式,它形式上是 T.f 或者 (*T).f,它没有指明具体接收者,只是表示接收者应该是类型T,同时,使用的时候第一个形参,必须是类型为T的实例。把原来方法的接收者替换成函数的第一个形参,好处是可以让方法表达式根据情况对应不同的接收者(只要类型相同)。

package main

import (
	"fmt"
)

func main() {
	path1 := Path{
		{1, 2},
		{3, 4},
		{2, 5},
	}
	path2 := Path{
		{1, 2},
		{3, 4},
		{2, 5},
	}
	path1.TranslateBy(Point{6, 6}, true)
	path2.TranslateBy(Point{6, 6}, false)
	fmt.Println(path1) // [{7 8} {9 10} {8 11}]
	fmt.Println(path2) // [{-5 -4} {-3 -2} {-4 -1}]
	f := Point.Add     // 方法表达式
	p := Point{0, 0}
	fmt.Println(f(Point{2, 2}, Point{3, 3})) // {5 5}
	fmt.Printf("%T\\n", p.Add)                // func(main.Point) main.Point
	fmt.Printf("%T\\n", f)                    //  func(main.Point, main.Point) main.Point
	fmt.Printf("%T\\n", foo)                  //  func(main.Point, main.Point) main.Point
}

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} }
func foo(p, q Point) Point        { return Point{} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) {
	var op func(q, p Point) Point
	if add {
		op = Point.Add
	} else {
		op = Point.Sub
	}
	for i := range path {
		path[i] = op(path[i], offset)
		if i == 0 {
			fmt.Printf("%T\\n", op) // func(main.Point, main.Point) main.Point
		}
	}
}

从上面的代码中,我们可以发现,方法变量(p.Add)和方法表达式(Point.Add)的函数签名类型是不同的,方法表达式(Point.Add)和第一形参为Point类型的普通方法(foo)的函数签名类型是相同的。所以,方法表达式的意义在于,为某个类型添加了一个方法,可以通过方法表达式快速转换生成一个普通的函数,它的第一形参是那个类型,并且可以绑定到该类型的不同实例上去

封装

我们知道,对于包,首字母大写的变量和函数是导出的。对于结构体,规则也如此,并且这是golang唯一的封装手段。要封装一个对象,必须用结构体,里面的字段或方法,根据首字母大小写控制对外的可见性。当然,一旦字段或方法从结构体导出的,那它就是包一级的了,所以,本质上golang封装的单元是包而不是类型。

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 }

对被封装的变量的操作,都是通过暴露的方法(大写字母开头的导出的方法)进行的。在这些方法中,最常见的是两种方法 getter 和 setter,golang的习惯是,对 getter 方法,省略前缀 Get,对于 setter 方法,则有前缀 Set

func (l *Logger) Flags() int
func (l *Logger) SetFlags(flag int)
func (l *Logger) Prefix() string
func (l *Logger) SetPrefix(prefix string)

我们可以在golang中导出字段,但导出字段对于API要慎重,因为它可能对兼容性是个问题(你无法像方法那样隐藏具体细节)

以上是关于golang学习随便记7的主要内容,如果未能解决你的问题,请参考以下文章

golang学习随便记14

golang学习随便记14

golang学习随便记3

golang学习随便记9

golang学习随便记12

golang学习随便记1