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