概念
方法的声明和函数极为类似,区别在于在函数名称前指定了一个类型,意思就是把这个函数绑定到了这个类型上,这就是方法。这个就是OOP编程思想的体现,让方法属于某一个类型,会Java的同学可能会非常熟悉这种感觉。
package main
import (
"fmt"
"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)
}
func main() {
p := Point{12,16}
q := Point{14,16}
distance := p.Distance(q)
fmt.Println(distance)
}
附加的参数p称为方法的接收者,源于最早的面向对象的语言。
值得注意的是,Go语言中,接收者不食用特殊名(比如this或者self),而是我们自己选择接收者名字,就像其他参数变量一样。为了方便我们自己使用,我们最好给这个方法接收者起一个简短、明了的名称。
指针接收者的方法
由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者如果一个实参太大而我们希望避免赋值整个实参,因此我们必须使用指针来传递变量的地址。这样同样适用于更新接收者:我们将它绑定到指针类型,比如:*Point。
func (p *Point) ScaleBy(factor float64 ){
p.X *= factor
p.Y *= factor
}
上面的方法的调用方式为,(*Point).ScaleBy
,圆括号是必须的,没有圆括号,表达式会被解析为*(Point.ScaleBy)。
*和&的区别 :
- & 是取地址符号 , 即取得某个变量的地址 , 如 ; &a
- *是指针运算符 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值 .
综上所述,就是在Go语言里面,如果我们想在函数或者方法里面修改参数的值,那么必须该参数是引用类型的,否则修改的只是形参的副本。
nil是一个合法的接收者
就像一些函数允许nil
指针为实参,方法的接收者也一样,尤其是当nil
是类型中有意义的零值(如map
和slice
类型)时更是如此。
package main
import "fmt"
//IntList是一个整型链表
//*IntList的类型nil是空列表
type IntList struct {
Value int
Tail *IntList
}
//Sum返回列表元素的总和
func (i *IntList) Sum() int {
if i == nil {
return 0
} else {
return i.Value + i.Tail.Sum()
}
}
func main() {
p := &IntList{10, nil}
list := &IntList{
5, p,
}
sum := list.Tail.Sum()
i := list.Value + sum
fmt.Println(i)
}
通过上面的代码我们能够清晰的了解到对于Go语言而言,nil
可以是合法的接收者,这在前面的文章我们进行了详细的描述。
位向量
位向量使用一个无符号整形值的slice,每一位代表了集合中的一个元素。
下面的程序演示了一个含有三个方法的简单位向量类型。
package main
//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添加非负数到集合中
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)
}
}
}
func main() {
}
由于每一个字拥有64位,因此为了定位x的位置,我们使用商数x/64作为字的索引,而x%64记作该字内位的索引。
UnionWith
操作使用按位或操作符|
来计算一次64个元素求并集的结果。
封装
如果变量或者方法是不能通过对象访问到的,这称作封装的变量或者方法。封装(有时候成为数据隐藏)是面向对象编程中重要的一方面。
Go语言只有一种方式控制命名的可见性:定义的时候,首字母大写的标识符是可以从包中导出的,而首字母没有大写的则不导出。同样的机制也同样作用于结构体内的字段和类型中的方法。
结论就是:要封装一个对象,必须使用结构体。
封装提供了三个优点:
- 因为使用方不能直接修改对象的变量,所以不需要更多的语句来检查变量的值。
- 隐藏实现细节可以防止使用方依赖的属性发生改变,使得设计者可以更加灵活地改变API的实现而不破坏兼容性。如下:
type Buffer struct{
buf []byte
initial [64]byte
/*...*/
}
//Grow方法按需扩展缓冲区的大小
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
}
}
- 防止使用者肆意的改变对象内的变量。因为对象的变量只能被同一个包内的函数修改,所以包的作者能够保证所有的函数都可以维护对象内部的资源。如下:
type Counter struct{ n int}
func (c *Counter) N() int {return c.n}
func (c *Counter) Increment() int {return c.n++}
func (c *Counter) Reset() int {return c.n = 0}
值得注意的是,在Java里面对于成员变量的获取和修改需要使用getter()
和setter()
,但是在Go语言里面,一般会将get前缀省略掉,如下:
package main
type Logger struct {
flags int
prefix string
//..
}
func (l *Logger) Flags() int { return l.flags }
func (l *Logger) SetFlags() { l.flags = 1 }
func (l *Logger) Prefix() string { return l.prefix }
func (l *Logger) SetPrefix() { l.prefix = "mua~" }
func main() {}
以上就是关于方法的一些介绍,希望可以帮助到你~