Go方法

##6.1方法定义

func (变量名 方法所属的类型)  方法名 (参数列表)(结果列表){
	...
}
eg:
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也被称为犯法的接收器【receiver】,在早期的面相对象语言中,它将调用方法描述为“向对象发送消息”.
Go中并不适用this或者self这样的特殊关键字来表示这个接收器【receiver】,我们可以根据自己喜好自定义这个参数名,一般而言,出于对一致性和简短的需要,我们使用类型的首字母,例如上面的例子使用的事Point类型的首字母p

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q)) // "5", method call

上面示例中的调用并不会冲突,第一个是函数调用,调用的是包级别的packageName.Distance。而第二个则是方法调用,调用的则是结构体Point下的Distance方法。
像上述的p.Distance表达式也被称为选择器【selector】,因为他为Point类型的接收器p选择合适的Distance方法。
选择器【selector】也被用于选择结构体类型的属性,例如p.X
但是因为方法和属性共享相同的命名空间,因此属性名不能与方法名重复!!
因为每种类型都有其方法的命令空间,所以同样的方法名,我们可以用于不同的其他类型。

在为任意类型定义方法这一点上,Go与其他的面相对象语言不太一样,Go允许我们为一些简单的数值、字符串、切片、map来定义一些附加的行为。

可以为在同一包中定义的任何具名类型声明方法,只要其底层类型既不是指针也不是接口即可。

方法之于函数的一个好处是:方法允许我们省略掉包名,简短我们的调用长度。

##6.2基于指针接收器的Method
Go中的函数是值传递的,如果我们需要更新一个变量,或者实参太大,我们想避免复制,那么指针就是解决方案。同样,如果我们想更新方法的接收器变量,那么自然也是使用指针,即我们将方法绑定到指针类型上:

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

}

该方法的名称为:(*Point).ScaleBy,括号是必须的
可以出现在接收器声明中的接收器只有具名类型以及指向它的指针这两种。
为了避免歧义,如果一个类型就是指针类型,那么不可以将其作为方法的接收器:

type Pointer *Point
func (p Pointer) point(){ //无法编译通过,因为Point类型自身就是指针
	...
}

当方法的接收器是一个具名类型的指针时,我们可以通过指针变量来调用方法:

func main(){
	p := &Point{10,10}
	p.ScaleBy(2)
}

同时,Go语言也为这种情况提供了语法糖,如果p是具名类型的变量名,而方法的接收器类型是该具名类型的指针,那么我们可以这样调用:

func main(){
	p := Point{10,10}
	p.ScaleBy(2)     //直接调用即可
}

编译器会隐式的用&p来调用ScaleBy方法。但是该语法糖只能适用于“变量”,包括结构体的属性,如p.X,或者是切片的元素,如perim[0]。但是对于非“变量”,我们无法获得临时值的地址,自然无法适用该语法糖,下面是对照样例:

	point := method.Point{1, 2}    //正确的
	point.ScaleBy(1)
	
	method.Point{1,1}.ScaleBy(10)  //错误的

相似的,如果方法的接收器是具名类型,我们可以使用具名类型变量来调用该方法(如下所示,我们适用*操作符取得了该指针所指向的值):

	pointer := &method.Point{1,1}				//指针变量
	(*pointer).Distance(method.Point{1,2})//Distance方法的接收器是具名类型,我们在这里通过*操作符来获取该指针所指向的值

同时,Go语言也为这种情况提供了语法糖,即可以使用指针变量来调用以具名类型作为接收器的方法:

	pointer := &method.Point{1,1}				//指针变量
	pointer.Distance(method.Point{1,2})//Distance方法的接收器是具名类型

编译器会隐式的用(*pointer)来调用Distance方法。

如果具名类型T的所有方法的接收器都是T自身(而不是*T),那么复制该类型的实例是安全的;调用它的任何方法都需要做复制,因为Go语言是值传递的。例如,time.Duration类型的值可以被任意的复制,包括作为函数参数的情况。但是如果方法的接收器是指针,你应当避免复制T的实例,因为这样做可能违反内部不变性。例如我们复制bytes.Buffer实例,可能会导致原始实例和拷贝对象只不过是对于同一个底层字节数组的别名而已(指针的复制就是这样,复制后的指针只不过是其所实际指向的内存的另一个别名而已)
###6.2.1 Nil也是一个有效的接收器值
正如函数可以允许Nil指针作为参数一样,方法同样允许Nil指针作为接收器,尤其是当Nil对于对象涞水是其合法的零值得时候,例如切片和map

//url包的Value
type Values map[string][]string
func (v Values) Get(key string) string {
	if v == nil {
		//当为nil,因为他是该map的有效零值,故为其提供空的默认值
		return ""
	}
	//如果长度大于0,则返回首个元素,否则返回空串
	vs := v[key]
	if len(vs) == 0 {
		return ""
	}
	return vs[0]
}
func (v Values) Add(key, value string) {
	v[key] = append(v[key], value)
}

测试

	values := url.Values{"name": {"piemon", "anokata"}}
	values.Add("city","qingdao")
	values.Add("city","beijing")

	fmt.Println(values.Get("name")) //piemon
	fmt.Println(values.Get("like")) //""
	fmt.Println(values["name"]) //piemon,anokata
	fmt.Println("================================")
	values = nil
	fmt.Println(values.Get("name")) //""   
	values.Add("like","banana")     //panic

因为url.Values是一个map类型,而map间接的引用它的键值对,任何对于url.Values的更新和删除都会在调用者处可见(即远处修改)。但是,与普通函数一样,方法对引用本身所做的任何更改,如将其设置为nil或使其引用不同的map数据结构,这都不会在调用者处可见。
##6.3通过结构体内嵌来拓展类型

type Point struct {X,Y float64}
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

}
type ColoredPoint struct {
	Point				//内嵌属性,我们可以获得X,Y这两个属性
	Color color.RGBA
}

我们定义的ColoredPoint 结构体共有三个属性,分别是X,Y,Color,我们适用内嵌的Point来提供X,Y这两个属性。
我们可以以简短的形式,不需要提及到Point,就可以访问到X,Y这两个属性,如下:

	coloredPoint := method.ColoredPoint{method.Point{1, 1}, color.RGBA{0, 0, 0, 0}}
	coloredPoint.X = 2
	coloredPoint.Y = 2
	coloredPoint.Point.X = 3
	coloredPoint.Point.Y = 3

	coloredPoint.ScaleBy(2)
	fmt.Println(coloredPoint) //{{6 6} {0 0 0 0}}

	

同样,该机制不仅对于属性有效,对于方法同样有效,我们可以适用ColoredPoint实例调用其内嵌类型的方法。
Point的方法被也被引入到ColoredPoint中。通过这种方式,嵌入允许利用多个字段的复合来构建包含许多方法的复杂类型,其中每个字段都会提供一些方法。

需要注意的是,ColoredPoint并不能类比的当做Point的子类,虽然它可以拥有Point的方法与属性,但这是组合,表示的是一种“has a”关系,而非“is a”关系。注意下面对Distance的调用。Distance有一个类型为Point的参数,而coloredPoint并不是一个Point,所以尽管coloredPoint有一个Point类型的内嵌字段,但是我们必须显式地选择它:

coloredPoint.Distance(coloredPoint)//错误:编译不通过
coloredPoint.Distance(coloredPoint.Point) //正确

ColoredPoint有可以获得Point的方法,ScaleBy和Distance,如果您喜欢从实现的角度考虑,那么嵌入的属性指示编译器生成附加的包装方法,这些包装方法将委托给内嵌属性所声明的方法,相当于:

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

在类型中内嵌的属性也可以是一个具名类型的指针,这种情况下,属性和方法会间接的引入到当前类型中。添加另一个间接层可以让我们共享通用结构并动态地改变对象之间的关系。如下:

type PointedPoint struct {
	*Point
	color.RGBA
}

func main(){
	p1 := method.PointedPoint{&(method.Point{2, 2}), color.RGBA{0, 0, 0, 0}}
	p2 := method.PointedPoint{&(method.Point{2, 2}), color.RGBA{0, 0, 0, 0}}
	p1.ScaleBy(2) 				//{{4,4},{0,0,0,0}}
	fmt.Println(*p1.Point)		//{4,4}
	p1.Point = p2.Point			//p1 :{{2,2},{0,0,0,0}}  p2 :{{2,2},{0,0,0,0}}  p1,p2共享相同的通用结构
	fmt.Println(*p1.Point)		//{2,2}
	p1.ScaleBy(3)				//p1 :{{6,6},{0,0,0,0}}  p2 :{{6,6},{0,0,0,0}} 
	fmt.Println(*p2.Point)		//{6,6}
}

PointedPoint 类型有Point的方法,也有color.RGBA的方法,还直接包含其自身所定义的方法。当Go编译器在处理一个像p.ScaleBy这样的解析器语句时候,他会首先检索在这个PointedPoint 类型中定义的ScaleBy方法,然后去检索由其内嵌属性所引入的方法,然后一直递归的找下去。

方法可以再具名类型或者其指针类型上声明,但是因为有了内嵌,使得我们可以让非具名【unnamed】的结构体类型具有方法:

var (
	lock sync.Mutex
	mapping = make(map[string]string)
)
func LookUp(key string) string {
	lock.Lock()
	defer lock.Unlock()
	value := mapping[key]
	return value
}

内嵌形式改造:
var cache = struct { //该匿名结构体,可以具有sync.Mutex的方法
	sync.Mutex
	mapping map[string]string
}{mapping :make(map[string]string)}

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

##6.4方法值与方法表达式
一般情况下,我们选择方法,并调用该方法的动作是在同一个表达式中做的,例如:

type Path struct{X,Y float64}
func (p Point) Distance (q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}
	p := method.Point{1,1}
	q := method.Point{2,2}
	p.Distance(q)

但是其实我们可以将其分为两步操作,p.Distance叫做“选择器【selector】”,他会产出一个方法值【method value】----> 即一个将方法( Point.Distance)绑定到特定接收器§的函数。这个函数可以不通过指定接收器,就可以被调用,如下:

	distance := p.Distance
	f := distance(q)

当程序中需要使用Delegate[委派]时候,即将对某个方法的调用,委派给另一个方法代为调用,这时候我们需要告诉这个方法所委派的方法的方法值。

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)    				相等的

与方法值相关的另一个概念是方法表达式,调用方法与调用函数的区别是,我们必须通过选择器(即p.Distance)语法来指定方法的接收器。
当类型是T时,方法表达式可以写作T.f或者(*T).f。该表达式会返回一个函数值,这种函数会将首参数作为接收器,所以通常可以这样用:

	distances := method.Point.Distance
	distances(p,q)

当您需要一个值来表示属于同一类型的多个方法之间的选择时,方法表达式是很有用的,这样您就可以使用许多不同的接收器调用所选择的方法。在下面的示例中,变量op表示类型Point中的加/减方法。Path.TranslateBy会为其内部的每一个Path应用这个方法。

type Path []Point
func (p Point) Add(q Point) Point {
	return Point{p.Y + q.X,p.Y + q.Y}
}

func (p Point) Sub(q Point) Point {
	return Point{p.Y - q.X,p.Y - q.Y}
}


func (path Path) TranslateBy (offset Point, add bool) {

	var method func(p,q Point) Point
	if add {
		method = Point.Add
	} else {
		method = Point.Sub
	}

	for i := range path {
		path[i] = method(path[i], offset)
	}

}

func main(){
		paths := method.Path{{1, 1}, {2, 2}}
	paths.TranslateBy(method.Point{10,10},true)
	for _,value := range paths {
		fmt.Println(value)
	}
}

log:
{11 11}
{12 12}

##6.6封装
如果一个对象的方法或者属性对其客户端而言是不可见的,那么可以说他是被封装【Encapsulation】的。封装有时也被称为信息隐藏【information hiding】,是面向对象的一个重要特性。

Go语言是通过首字母大小写来控制访问权限的:大写表示包内外都可见,而小写仅仅表示包内可见。
该机制也用于限制对结构体的属性或类型的方法的访问。因此,要封装一个对象,我们必须使其为struct

方式一
type IntSet struct {
	words []uint64
}
方式二
type  IntSet   []uint64

虽然这两种形式是等价的,但是,通过方式二的IntSet ,我们可以再任意的包中使用,并且可以允许客户端直接读取和更新该切片。方式一的IntSet 虽然可以在任意包中引用,但是其words属性是被封装的,因此无法在其他包中直接操作该切片,更安全。

使用名称大小写的机制的结果是使得封装单元是包级,而不是如其他语言中的类型级。一个结构体的属性通同一包中的所有代码可见,无论是你的代码是写在函数㕜,还是方法中。
封装的三个优点:

  1. 因为客户端无法直接的修改对象的变量,只需要检查更少的语句来理解这些变量的可能值。
  2. 隐藏实现的细节,防止调用方依赖那些可能变化的实现细节,这让程序员在不破坏API兼容性的情况下更自由地发展实现。
  3. 阻止客户端任意的修改对象的变量值。因为对象的变量值值可以被同一包中的函数会方法来设置,因此包的设计者可以确保同一包中的所有函数保证对象的内部不变量
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 }

如上的Counter 允许客户端重置或者递增该计数器,但是不允许客户端任意的直接修改计数器的值。
Go 的编码风格并不禁止导出字段。当然,一旦该属性字段被导出了,那么该字段不能被一个的未导出 API更改,所以最初的选择应该是深思熟虑,并且还要考虑到维护包内不变量的复杂性,未来可能的变化以及和客户端代码是否会受到变更的影响。

猜你喜欢

转载自blog.csdn.net/qq_31179577/article/details/82843452
今日推荐