Go

Go语言的方法

Posted by BY on February 16, 2018

方法中的自己的总结

方法的声明

方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数。这个参数吧这个方法绑定到这个参数对应的类型上:

package main

import "math"

type Point struct {
	X, Y float64
}
// 普通的函数
func Distance(q, p 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{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p,q)) // 5
fmt.Println(p.Distance(q)) // 5
}

上面两个Distance函数声明并不冲突,第一个声明一个包级别的函数,第二个声明一个类型Point的方法。它的名字是Point.Distance

p.Distance称作选择子,因为它为接收者p选择合适的Distance方法。

例子,定义一个path类型表示一条线,同样也用Distance作为方法名:

package main

import (
	"math"
	"fmt"
)

type Point struct {
	X, Y float64
}
// 普通的函数
func Distance(q, p Point) float64{
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// Point类型方法
func (Q Point) Distance(q Point) float64 {
	return math.Hypot(q.X-Q.X, q.Y-Q.Y)
}

type Path []Point
func (path Path) Distance() float64  {
	sum := 0.0
	for i := range path {
		if i > 0 {
			sum += path[i-1].Distance(path[i])
		}
	}
	return sum
}

func main() {
	a := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}
	fmt.Println(a.Distance()) // 12
}

Go语言可以将方法绑定到任何类型上,可以很方便的对想要的类型定义附加行为。编译器会通过方法名和接收者的类型决定调用哪一个函数。

不同的类型可以使用相同的方法名。

指针接收者的方法

由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者如果一个实参太大而我们希望避免复制这个实参,因此我们必须使用指针来传递变量的地址。这也同样适用于更新接收者:我们将它绑定到指针类型:

package main

import (
	"fmt"
)

type Point struct {
	X, Y float64
}

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

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

也可以:

package main

import (
	"fmt"
)

type Point struct {
	X, Y float64
}

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

func main() {
	r := Point{1, 2}
	tr := &r
	tr.ScaleBy(2)
	fmt.Println(r) // {2 4}
}

nil是一个合法的接收者

方法允许nil作为实参,在整型数链表中,nil代表空链表:

package main


type IntList struct {
	Value   int
	Tail    *IntList
}

func (l *IntList) Sum() int  {
	if l == nil{
		return 0
	}
	return l.Value + l.Tail.Sum()
}

在net/url包中Values类型的部分定义:

package main

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

进行测试:

func main() {
	m := 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]

	m = nil
	fmt.Println(m.Get("item")) //
}

通过结构体内嵌组成类型

type Point struct {
	X, Y  float64
}
type ColoredPoint struct {
	Point
	Color   color.RGBA
}

内嵌了一个Point类型,使结构体ColoredPoint包含有其它更多的字段,并且可以直接引用:

func main() {
	var cp ColoredPoint
	cp.X = 1
	fmt.Println(cp.Point.X) // 1
	cp.Point.Y = 3
	fmt.Println(cp.Y) // 3
}
package main

import (
	"image/color"
	"fmt"
	"math"
)

type Point struct {
	X, Y  float64
}
type ColoredPoint struct {
	Point
	Color   color.RGBA
}
func (p *Point) ScaleBy(factor float64)  {
	p.X *= factor
	p.Y *= factor
}
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func main() {
	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
}

可用ColoredPoint声明内嵌 *Point:


import (
	"image/color"
	"fmt"
	"math"
)

type Point struct {
	X, Y  float64
}
type ColoredPoint struct {
	*Point
	Color   color.RGBA
}
func (p *Point) ScaleBy(factor float64)  {
	p.X *= factor
	p.Y *= factor
}
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func main() {
	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.Point = q.Point // q和p共享一个Point
	p.ScaleBy(2)
	fmt.Println(*q.Point, *p.Point) // {10 8} {10 8}
}

方法只能在命名的类型(如 Point)和指向它们的指针( *Point)中声明,但内嵌帮助我们能够在未命名的结构体类型中声明方法

方法变量与表达式

方法变量:把方法绑定到一个接收者上,这样做有一个”拖延“的作用。:

package main

import (
	
	"fmt"
	"math"
)

type Point struct {
	X, Y  float64
}
func (p *Point) ScaleBy(factor float64)  {
	p.X *= factor
	p.Y *= factor
}
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func main() {
	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.23606797749979"(根号5)

	scaleP := p.ScaleBy  // 方法变量
	scaleP(2)
	fmt.Println(p.X,p.Y) // 2 4
	scaleP(3)
	fmt.Println(p.X,p.Y) // 6 12
	scaleP(10)
	fmt.Println(p.X,p.Y) // 60 120
}

方法变量相关的是方法表达式,在调用方法时必须提供接收者,并按照选择子的语法进行调用,一般写作T.f或者(*T).f,其中T是类型:

package main

import (

	"fmt"
	"math"
)

type Point struct {
	X, Y  float64
}
func (p *Point) ScaleBy(factor float64)  {
	p.X *= factor
	p.Y *= factor
}
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func main() {
	p := Point{1,2}
	q := Point{4,6}
	distance := Point.Distance // 方法表达式
	fmt.Println(distance(p, q)) // 5
	fmt.Printf("%T\n", distance) // func(main.Point, main.Point) float64

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

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

type Point struct {
	X, Y  float64
}
func (p Point) Add(q Point) Point {
	return Point{q.X+p.X, q.Y+p.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] = op(path[i],offset)
	}
}

根据bool值,选择是用path[i].Add(offset)或者是path[i].Sub(offset)

示例_位向量

位向量使用一个无符号整型值的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记作该字内位的索引。操作符|=按位或(或运算是0-0-0,其他都是1,正好可以去重),计算一次64个元素求并集的结果。

写一个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()
}

演示:

func main() {
	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
}

封装

如果变量或方法是不能通过对象访问到的,这称作封装的变量或方法。

  • 要封装一个对象,必须是结构体。

  • 在Go中封装的单元是包而不是类型。无论是在函数内的代码还是方法内的代码,结构体类型内的字段对于同一个包中的多有代码都是可见的。

封装的优点

  1. 因为使用方不能直接修改对象的变量,所以不需要更多的语句用来检查变量的值。
  2. 隐藏细节可以防止使用方依赖的属性发生改变,使得设计者可以更加灵活地改变API的实现而不破坏兼容性。使用者使用时更多的感觉是性能的提升,而不关心其中的实现。
  3. 防止使用者肆意改变对象内的变量。