Dawn's Blogs

分享技术 记录成长

0%

GO语言圣经学习笔记 (5) 方法和封装

方法

方法是与类型相关联的函数

方法声明

一个方法的声明如下,其中参数p,叫做方法的接收器(receiver):

1
2
3
4
5
6
7
8
9
10
import "math"

type Point struct {
X, Y float64
}

// 一个方法的声明
func (p Point) Distance(q Point) float64 {
return math.Htpot(q.X-p.X, q.Y-p.Y)
}

可以给同一个包内的任意命名类型(基础数据类型需要起别名)定义方法,只要这个命名类型的底层类型不是指针或者interface

指针类型的接收器

也可以定义接收器对象为指针类型,这样可以避免拷贝、或者更新接收器对象:

1
2
3
4
5
// 接收器为指针类型
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}

注意:

  • 不管方法的接收器是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会自动做隐式类型转换

  • nil可以作为接收器

嵌入结构体扩展类型

可以嵌入其他结构体,嵌入匿名结构体时,可以直接访问叶子属性而不需要给出完整的路径、也可以拥有匿名结构体的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
type Point struct{
X, Y float64
}

type RGBA struct {
R, G, B, A uint8
}

type ColoredPoint struct {
Point // 嵌入匿名结构体
Color color.RGBA
}

同时,可以把ColoredPoint类型当作接收器来调用Point里的方法,即使ColoredPoint里没有声明这些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 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"

/*
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
*/

一个ColoredPoint并不是一个Point,但他**”has a”Point**,并且它有从Point类里引入的DistanceScaleBy方法。Distance函数的参数类型是Point,所以必须显式地选择q.Point

方法值和方法表达式

方法值

常用p.Distance()调用方法,实际上将其分成两步来执行也是可能的。p.Distance叫作选择器,选择器会返回一个方法值:一个将方法Point.Distance绑定到特定接收器变量的函数。

这个函数可以不通过指定其接收器即可被调用,因为已经在前文中指定过了,只要传入函数的参数即可

1
2
3
4
5
6
7
8
9
10
11
12
p := Point{1, 2}
q := Point{4, 6}

distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5)

scaleP := p.ScaleBy // method value
scaleP(2) // p becomes (2, 4)
scaleP(3) // then (6, 12)
scaleP(10) // then (60, 120)

方法表达式

当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数”值”,这种函数会将其第一个参数用作接收器

1
2
3
4
5
6
7
8
9
10
11
p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"

当你根据一个变量来决定调用同一个类型的哪个函数时,方法表达式就显得很有用了。可以根据选择来调用接收器各不相同的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 {
// Call either path[i].Add(offset) or path[i].Sub(offset).
path[i] = op(path[i], offset)
}
}

封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为封装

Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种基于名字的手段使得在语言中最小的封装单元是package,而不是像其它语言一样的类型,如一个struct类型的字段对同一个包的所有代码都有可见性。

封装的好处

  • 隐藏实现细节

  • 可以对数据(某些字段)进行验证,保证安全合理

封装的步骤

  1. 将结构体、字段的首字母小写,使之不能导出
  2. 给结构体所在包提供一个工厂模式的函数,首字母大写,类似于构造函数
  3. 提供访问getter或修改setter内部变量的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package person

import "fmt"

// 定义不能导出的结构体
type person struct {
name string
age int
sal float64
}

// 定义工厂模式的函数 首字母大写 类似构造函数
func NewPerson(name string) *person{
return &person{
name:name,
}
}

// 提供一个setter 设置年龄
func (user *person) Setage(age int) {
if age >0 && age < 150 {
user.age = age
}else {
fmt.Println("年龄数值不对!")
}
}

// 获取年龄
func (user *person) Getage() int{
return user.age
}

// 更改姓名
func (user *person) Updatename(name string) string{
user.name=name
return user.name
}