Go 方法

Go 语言也支持面向对象的思想;
所谓面向对象编程:
1对象就是简单的一个值或者变量,并且拥有其方法
2方法是某种特定类型的函数
3

面向对象编程就是使用方法来描述每个数据结构的属性和操作;

使用者不需要了解对象本身的实现

   

1.1 方法声明

方法声明基础语法 :

    方法的声明和普通函数的声明类似,只是在函数名字前面多了一个(类型)参数

    这个参数把这个方法绑定到对应的类型上

示例 :现在尝试在一个与平面几何相关的包中写第一个方法

package geometry

import "math"

type Point struct{ X ,Y float64 }

// 普通的函数定义
func Distance(p,q Point) float64 {
    return math.Hypot(q.X - p.X ,q.Y - p.Y)
}

// Point 类型的方法
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X - p.X ,q.Y - p.Y)
}

方法声明中的一些要点:

a). 附加的参数 p 称为方法的接收者,用来描述主调方法向对象发送消息

b). Go 语言中,接收者不使用特殊名(比如 this 或 self);我们自己选择接收者名字,就像其他的参数变量名一样

c). 接收者会频繁地使用,因此最好能够选择简短且在整个方法中名称始终保持一致的名字;最常用的办法是取类型名称的首字母(小写),就像 Point 中的 p

方法调用要点 :

a). 调用方法的时候,接收者在方法名的前面;这样,调用就和声明保持顺序一致了

p := Point{1,2}
q := Point{3,4}
fmt.Println(Distance(p,q))    // 函数调用
fmt.Println(p.Distance(q))     // 方法调用

方法调用

上面两个 Distance 函数声明没有冲突
第一个声明了一个包级别的函数(geometry.Distance)
第二个声明了一个类型 Point 的方法,其名字为 Point.Distance
表达式 p.Distance 称为选择子(selector),因为 p.Distance 为接收者 p 选择合适的 Distance 方法
选择子语法也用于选择结构类型中的某些字段值,就像 p.X 中的字段值

由于方法和字段来自于同一个命名空间(在同一个结构类型中),因此在 Point 结构类型中声明一个叫 X 的方法会与字段 X 冲突,编译器会报错

小结 :同类型中,不能存在同名的标识符

因为每一个类型有其自己的命名空间,所以我们能够在其他不同的类型中使用名字 Distance 作为方法名

定义一个 Path 类型表示一条线段,同样也使用 Distance 作为方法名

// Path 是连接多个点的直线段

type Path [ ]Point

// Distance 方法返回路径的长度

func (path Path) Distance() float64 {

        sum  :=  0.0

        for  i  :=  range  path  {

                if i  >  0 {

                        sum  +=  path[i-1].Distance(path[i])

                }

        }

        return  sum

}

path 是一个命名的 slice 类型,而非 Point 这样的结构体类型,但我们依旧可以给它定义方法

Go 和许多其他面向对象的语言不同;

在 Go 语言中,可以将方法绑定到任何类型上

我们可以很方便地为简单的类型(如数字 、字符串 、slice 、map ,甚至函数等)定义附加的行为(方法)
同一个包下的任何类型都可以声明方法,只要它的类型既不是指针类型也不是接口类型

这两个 Distance 方法(Point.Distance 和 Path.Distance)拥有不同的类型;

它们彼此无关,尽管 Path.Distance 在内部使用 Point.Distance 来计算线段相邻点之间的距离

总结:

在同一个结构类型中,

字段与字段不能同名

方法与方法不能同名

字段与方法不能同名

但在两个不同的结构类型中,可以存在相同名称的字段或方法,前提是相同的字段或方法,所属的类型不同

1.2 指针接收者的方法

背景 :

    主调函数会复制每一个实参变量

    如果函数需要更新一个变量,或者如果一个实参太大而我们希望避免复制整个实参,则我们必须使用指针来传递变量的地址;这也同样适用于更新接收者 :我们将方法绑定到指针类型,比如 *Point

func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

注意 :

    这个方法的名字是 (*Point).ScaleBy ;圆括号是必需的,没有圆括号,表达式会被解析为 *(Point.ScaleBy)

Tips :

    在真实的程序中,习惯上遵循如果 Point 的任何一个方法使用指针接收者,那么所有的 Point 方法都应该使用指针接收者,即使有些方法不一定需要

    变量类型(Point)与指针类型(*Point)是唯一可以出现在接收者声明处的类型;不允许本身是指针的类型进行方法声明

type P *int
func (P) f() { /* ... */ }  // 编译错误:非法的接收者类型

    通过提供 *Point 可以调用 (*Point).ScaleBy 方法

r := &Point{1,2}
r.ScaleBy(2)
fmt.Println(*r)  // "{2,4}"

或者

p := Point{1,2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p)  // "{2,4}"

或者

p := Point{1,2}
(&p).ScaleBy(2)
fmt.Println(p)  // "{2,4}"

    后面两个用法虽然看上去比较别扭,却也是合法的

    如果接收者 p 是 Point 类型的变量,但方法要求一个 *Point 接收者,我们可以使用简写 :

    变量类型实参可以以句点形式直接调用指针类型方法

p := Point{1,2}
p.ScaleBy(2)

    说明:

    实际上编译器会对变量进行 &p 的隐式转换;

    只有变量才允许这么做,包括结构体字段,像 p.X 、数组 、slice 的元素,比如 perim[0]

    不能对一个不能取地址的 Point 接收者参数调用 *Point 方法,因为无法获取临时变量的地址

Point{1,2}.ScaleBy(2)  // 编译错误 :不能获得 Point 类型字面量的地址

    但是,如果实参接收者是 *Point 类型,以 Point.Distance 的方式调用 Point 类型的方法是合法的,因为我们有办法从地址中获取 Point 的值,只要解引用指向接收者的指针值即可;编译器自动插入一个隐式的 * 操作符;

    换句话说,变量可以直接以句点的方式调用接收者是指针类型的方法

    示例 :下面两个函数的调用效果是一样的

pptr.Distance(q)
(*pptr).Distance(q)

    总结一下这些例子 :在合法的方法调用表达式中,只有符合下面三种形式的语句才能够成立

(A). 实参接收者和形参接收者是同一个类型,比如都是 T 类型或都是 *T 类型

       方法声明中接收者的类型与实参的类型,两者一致

Point{1,2}.Distance(q)  // Point
pptr.ScaleBy(2)          // *Point

(B). 方法声明中接收者是指针类型(*T),实参是变量类型(T),变量类型实参直接以句点形式调用指针类型方法;编译器会隐式获取变量类型实参的地址

p.ScaleBy(2)  // 隐式转换为 (&p)

(C). 方法声明中接收者是变量类型(T),实参是指针类型(*T),指针类型实参亦直接以句点形式调用变量类型方法;编译器会隐式解引用指针类型实参,获得其实际的值

pptr.Distance(q)  // 隐式转换为 (*pptr)

注意 :

    如果所有类型 T 方法的(实参)接收者是 T 自己(而非 *T),也就是说接收者和实参都是变量类型,那么复制该类型的实例是安全的;调用方法的时候都必须进行一次复制

(示例 :time.Duration 的值在作为实参传递到函数的时候就会复制)

    但是,任何方法的接收者是指针类型时,应该避免复制 T 的实例;因为这么做可能会破坏内部原本的数据;随后的方法调用会产生不可预期的结果

(示例 :复制 bytes.Buffer 实例只会得到相当于原来 bytes 数组的一个别名;)

nil 是一个合法的接收者

    就像一些函数允许 nil 指针作为实参,方法的接收者也一样,尤其当 nil 是类型中有意义的零值(如 map 和 slice 类型)时,更是如此

示例 :在下面这个简单的整数链表中,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()
}

    当定义一个类型允许 nil 作为接收者时,应当在文档注释中显式地标明,如上面的例子所示

示例 :下面的代码是 net/url 包中 Values 类型的部分定义 :

package url 

// Values 映射字符串到字符串列表
type Values map[string][]string

// Get 返回第一个具有给定 key 的值
// 如不存在,则返回空字符串
func (v Values) Get(key string) string {
    if vs := v[key]; len(vs) > 0 {
        return vs[0]
    }
    return ""
}

// Add 添加一个键值到对应 key 列表中
func (v Values) Add(key, value string) {
    v[key] = append(v[key], value)
}

    它的实现是 map 类型,但也提供了一系列方法来简化 map 的操作,它的值是字符串 slice ,即一个多重 map ;使用者可以使用它固有的操作方式(make 、slice 字面量 、m[key] 等方式),或者使用它的方法,或同时使用

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")            // 宕机:赋值给空的 map 类型

    在最后一个 Get 调用中,nil 接收者充当一个空 map ;它可以等同地写成 Values(nil).Get("item") ,但是 nil.Get("item") 不能通过编译,因为 nil 的类型没有确定;相比之下,最后的 Add 方法会发生宕机,因为 Add 方法尝试更新一个空的 map

    因为 url.Values 是 map 类型而且 map 间接地指向它的键值对,所以 url.Values.Add 对 map 中元素的任何更新和删除操作,对调用者都是可见的

    然而,和普通函数一样,方法对引用本身做的任何改变(比如设置 url.Values 为 nil 或者使它指向一个不同 map 数据结构)都不会在调用者身上产生作用

1.3 通过结构体内嵌来组成(新)类型

    考虑 ColoredPoint 类型 :

import "image/color"

type Point struct{ X,Y float64 }

type ColoredPoint struct {
    Point
    Color color.RGBA
}

    我们只是想定义一个有三个字段的结构体 ColoredPoint ,但实际上我们内嵌了一个 Point 类型从而提供字段 X 和 Y ;内嵌可以更简单地定义 ColoredPoint 类型,ColoredPoint 类型包含了 Point 类型的所有字段以及其他更多的自有字段

    如果需要,可以直接使用(访问)ColoredPoint 内所有的字段,而不需要提到 Point 类型

    顶层变量可以以句点形式直接访问最终的成员,无须写出提供最终成员的内嵌类型

var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X)  // "1"
cp.Point.Y = 2
fmt.Println(cp.Y)    // "2"

    同理,这也适用于 Point 类型的方法;

    我们能够通过类型为 ColoredPoint 的接收者,直接调用内嵌类型 Point 的方法,即使在 ColoredPoint 类型没有声明过这个方法的情况下

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"

    Point 的方法都被纳入到 ColoredPoint 类型中;以这种方式,内嵌允许构成更复杂的类型,该复杂类型由许多字段构成,每个字段提供一些方法

说明 :

    熟悉基于类的面向对象编程语言的读者可能认为 Point 类型就是 ColoredPoint 类型的基类,而 ColoredPoint 则作为子类或派生类,或将这两个之间的关系翻译为 ColoredPoint 就是 Point 的其中一种表现;但这是个误解

    注意上面调用 Distance 的地方,Distance 有一个形参 Point ,q 不是 Point ,因此虽然 q 有一个内嵌的 Point 字段,但是必须显式地使用该 Point 字段;尝试直接传递 q 作为参数会报错

p.Distance(q)  // 编译错误: 不能将 q (ColoredPoint) 转换为 Point 类型

    ColoredPoint 并不是 Point ,但是 ColoredPoint 包含一个 Point ,并且 ColoredPoint 还有两个另外的方法 Distance 和 ScaleBy 也来自 Point ;

    如果考虑具体实现,实际上,内嵌的字段会告诉编译器,生成额外的包装方法来调用 Point 声明的方法,这相当于以下代码 :

func (p ColoredPoint) Distance(q Point) float64 {
    return p.Point.Distance(q)
}

func (p *ColoredPoint) ScaleBy(factor float64) {
    p.Point.ScaleBy(factor)
}

    当 Point.Distance 被上面的第一个包装方法调用的时候,接收者的值是 p.Point 而不是 p ;在 Point 类型的方法里,无法访问 ColoredPoint 的任何字段

    匿名字段类型可以是个指向命名类型的指针,此时,字段和方法会被间接地引入到当前的类型中;这可以让我们共享通用的结构以及使对象之间的关系更加动态 、多样化;

    示例 :下面的 ColoredPoint 声明,内嵌了 *Point

type ColoredPoint struct {
    *Point
    Color color.RGBA
}

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

    结构体类型可以拥有多个匿名字段;

    示例 :声明 ColoredPoint

type ColoredPoint struct {
    Point
    color.RGBA
}

    那么,这个 ColoredPoint 类型的实例可以拥有 Point 所有的方法和 RGBA 所有的方法,以及任何其他直接在 ColoredPoint 类型中声明的方法;当编译器处理选择子(比如 p.ScaleBy)的时候,首先,编译器先查找到直接声明的方法 ScaleBy ,之后再从来自 ColoredPoint 的内嵌字段的方法中进行查找,再之后从 Point 和 RGBA 中内嵌字段的方法中进行查找,以此类推;

    当同一个查找层级中存在同名方法时,编译器会报告选择子不明确的错误

    方法声明只能绑定到变量类型(比如 Point)或指针类型(比如 *Point)上,但内嵌允许在未命名的结构体类型中声明方法

    示例 :下面的例子展示了简单的缓存实现,其中使用了两个包级别的变量 -- 互斥锁和 map ,互斥锁将会保护 map 的数据

var (
    mu sync.Mutex  // 保护 mapping
    mapping = make(map[string]string)
)

func Lookup(key string) string {
    mu.Lock()
    v := mapping[key]
    mu.Unlock()
    return v
}

    示例 :下面版本的功能和上面的完全相同,但是将两个相关变量放到了一个包级别的变量 cache 中

var cache = struct {
    sync.Mutex
    mapping map[string]string
} {
    mapping: make(map[string]string),
}

func Lookup(key string) string {
    cache.Lock()
    v := cache.mapping[key]
    cache.Unlock()
    return v
}

    新的变量名更加贴切,而且 sync.Mutex 是内嵌的,它的 Lock() 和 Unlock() 方法也包含进了结构体中,允许我们直接使用 cache 变量本身进行加锁

1.4 方法变量与表达式

    通常,我们都在相同的表达式中使用和调用方法,就像在 p.Distance() 中,但也可以把两个操作分开;选择子 p.Distance 可以赋予一个方法变量,它是一个函数,把方法(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.23606" 

scaleP := p.ScaleBy    // 方法变量
scaleP(2)              // p 变成 (2, 4)
scaleP(3)              // 然后是 (6, 12)
scaleP(10)             // 然后是 (60, 120)

    如果包内的 API 调用一个函数值,并且使用者期望这个函数的行为是调用一个特定接收者的方法,方法变量就非常有用;

    示例 :比如,函数 time.AfterFunc 会在指定的延迟后调用一个函数值,下面这个程序使用 time.AfterFunc 在 10s 后启动火箭 r :

type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }

r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })

    如果使用方法变量则可以更加简洁 :

time.AfterFunc(10 * tiem.Sceind, r.Launch)

    与方法变量相关的是 "方法表达式";和调用一个普通的函数不同,在调用方法的时候必须提供接收者,并且按照选择子的语法进行调用;而方法表达式写成 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(Point, Point) float64"

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

    如果需要用一个值来代表多个方法中的一个,而方法属于同一个类型,方法变量可以帮助你调用这个值所对应的方法来处理不同的接收者;

    示例 :在下面这个例子中,变量 op 代表加法或减法,二者都属于 Point 类型的方法;Path.TranslateBy 调用了它计算路径上的每一个点

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

1.5 示例:位向量

    Go 语言的集合,通常使用 map[T]bool 来实现,其中 T 是元素类型

    使用 map 的集合,扩展性良好,但是对于一些特定问题,一个专门设计过的集合性能会更优;比如,在数据流分析领域,集合元素都是小的非负整型,集合拥有许多元素,而且集合的操作多数是求并集和交集,"位向量" 是个理想的数据结构

    位向量使用一个无符号整型值的 slice ,每一位代表集合中的一个元素;如果设置第 i 位的元素,则认为集合包含 i ;

    示例 :下面的程序演示了一个含有三个方法的简单位向量类型

// 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 记作该字内,位的索引;UnionWith 操作使用按位 "或" 操作符 | 来计算一次 64 个元素求并集的结果

    这个实现缺少许多需要的特性,有些会在练习中列出来,但是有一个特性不得不在这里提到 :以字符串输出 IntSet 的方法;添加一个 String 方法

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

    注意,上面的 String 方法和之前的 intsToString 函数相似;在 String 方法中 bytes.Buffer 经常以这样的方式用到;fmt 包把具有 String 方法的类型进行特殊处理,于是,即使是复杂类型也可以按照友好的方式显示出来;fmt 默认调用 String 方法而不是原生的值;这个机制需要依靠接口和类型断言

    现在,可以演示 IntSet 了

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"

    提醒一句:我们为指针类型 *IntSet 声明了 String 和 Has 方法并非出于需要,而是为了和其他两个方法保持一致,另外两个方法需要指针接收者,因为它们需要对 s.words 进行赋值;所以,IntSet 的值并不含有 String 方法,使用它可能会产生意料外的结果

fmt.Println(&x)          // "{1 9 42 144}"
fmt.Println(x.String())  // "{1 9 42 144}"
fmt.Println(x)           // "{[4298046511618 0 65536]}"

    第一个示例中,输出了 *IntSet 指针,它有一个 String 方法;第二个示例中,基于 IntSet 值调用  String() 方法;编译器会帮助我们隐式地插入 & 操作符,我们得到指针后就可以获取到 String 方法了;但在第三个示例中,因为 IntSet 值本身并没有 String 方法,所以 fmt.Println 直接输出结构体;因此,记得加上 & 操作符很重要;那么给 IntSet(而不是 *IntSet)加上 String 方法应该是个不错的主意,但这还需要根据实际情况而定

1.6 封装

    如果(成员)变量或者方法不能通过对象访问到,这称作 "封装" 的变量或者方法;封装(有时候也称作信息隐藏)是面向对象编程中重要的一方面

    Go 语言只有一种方式控制命名的可见性 :定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的标识符则不是导出的;相同的机制也同样作用于结构体内的字段和类型中的方法

    结论就是,要封装一个对象,必须将其定义为一个结构体

    这就是为什么上一节中 IntSet 类型被声明为结构体,但它只有一个字段 :

type IntSet struct {
    words []uint64
}

    可以重新定义 IntSet 为一个 slice 类型,如下所示,当然,必须把方法中出现的 s.words 替换为 *s

type IntSet []uint64

    尽管这个版本的 IntSet 和之前的基本等同,但是这个版本的 IntSet 允许其他包中的调用者读取以及修改这个 slice ;换句话说,表达式 *s 可以在其他包中使用,而 s.words 只能在定义 IntSet 的包中使用

    另一个结论是 :在 Go 语言中封装的单元是包而不是类型

    无论是在函数内的代码还是方法内的代码,结构体类型内的字段,对于同一个包中的所有代码都是可见的

    封装提供了三个优点 :

    第一 :因为调用方不能直接修改对象的(成员)变量,所以不需要更多的语句用来检查变量的值

    第二 :隐藏实现细节,可以防止调用方依赖的属性发生改变,使得设计者可以更加灵活地改变 API 的实现而不破坏兼容性

示例 :

    考虑 bytes.Buffer 类型;它用来堆积非常小的字符串,因此为了优化性能,实现上会预留一部分额外的空间避免频繁申请内存;由于 Buffer 是结构体类型,因此这块空间使用额外的一个字段 [64]byte ,且命名不是首字母大写;因为这个字段没有导出,bytes 包之外的 Buffer 使用者除了感觉到性能的提升之外,不会关心其中的实现;Buffer 和它的 Grow 方法如下 :

type Buffer struct {
    buf     []byte
    initial [64]byte
    /* ... */
}

// Grow 方法按需扩展缓冲区的大小
// 保证 n 个字节的空间
func (b *Buffer) Grow(n int) {
    if b.buf == nil {
        b.buf = b.initial[:0]  // 最初使用预分配的空间
    }
    if len(b.buf) + n > cap(b.buf) {
        buf := make([]byte, b.Len(), 2 * cap(b.buf) + n)
        copy(buf, b.buf)
        b.buf = buf
    }
}

    第三 :防止调用者肆意改变对象内的变量;因为对象中的变量只能由同一个包内的函数修改,所以包的作者可以保证所有的函数都可以维护对象内部的资源

示例 :

    比如,下面的 Counter 类型允许使用者递增计数或者重置计数器,但是不能够随意地设置当前计数器的值

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 ,就像 log 包里的 Logger 类型;然而,命名 getter 方法的时候,通常将 Get 前缀省略;这个简洁的命名习惯也同样适用在其他冗余的前缀上,比如 Fetch 、Find 和 Lookup

package log

type Logger struct {
    flags   int
    prefix  string
    // ...
}

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

    Go 语言也允许导出的字段;当然,一旦导出就必须要面对 API 的兼容问题,因此最初的决定需要慎重,要考虑到之后维护的复杂程度,将来发生变化的可能性,以及变化对原本到吗质量的影响等

    封装并不总是必需的;time.Duration 对外暴露 int64 的整型数用于获得微秒,这允许我们能够对其进行通常的数学运算和比较操作,甚至定义常数 :

const day = 24 * time.Hour
fmt.Println(day.Seconds())    // "86400"

    另一个例子可以比较 IntSet 和本章开头的 geometry.Path 类型;Path 定义为一个 slice 类型,允许它的使用者使用 slice 字面量的语法来构成实例,比如使用 range 循环遍历 Path 所有的点等,而 IntSet 则不允许这些操作

    有个明显的对比 :geometry.Path 从本质上讲,就只是连续的点,以后也不会添加新的字段,因此 geometry 包将 Path 的 slice 类型暴露出来是合理的做法;与它不同的是,IntSet 只是看上去像 []uint64 的 slice ,但它实际上完全可以是 []uint 或其他复杂的集合类型,而且另外用来记录集合中元素数量的字段充当了重要的作用;基于上述原因,IntSet 不对外透明也合情合理

总结 :

    在这一章中,我们学习了如何在命名类型中定义方法,以及如何调用这些方法;尽管方法是面向对象编程的关键,但这一章只讲述了其中的一部分内容;下一章会继续介绍 "接口" 相关的内容来完成这方面的学习

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值