Go语言之方法

185 阅读3分钟

概念

方法的声明和函数极为类似,区别在于在函数名称前指定了一个类型,意思就是把这个函数绑定到了这个类型上,这就是方法。这个就是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)。

*和&的区别 :

  1. & 是取地址符号 , 即取得某个变量的地址 , 如 ; &a
  2. *是指针运算符 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值 .

综上所述,就是在Go语言里面,如果我们想在函数或者方法里面修改参数的值,那么必须该参数是引用类型的,否则修改的只是形参的副本。

nil是一个合法的接收者

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

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{10nil}
 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 intbool {
 word, bit := x/64uint(x%64)
 return word < len(s.Words) && s.Words[word]&(1<<bit) != 0
}

//Add添加非负数到集合中
func (s *IntSet) Add(x int) {
 word, bit := x/64uint(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() {}

以上就是关于方法的一些介绍,希望可以帮助到你~

qrcode_for_gh_3fe38a18d553_258.jpg