命令记不住

登陆

Go 方法

方法

1 方法定义

对象的行为用方法表示,方法是某种特定类型的函数,面向对象编成就是使用方法来描述每个数据结构的属性和操作,使用者不需要了解对象本身的实现.

func (方法接收者)方法名(参数) (返回值){return 值}

值对象接收者,不可以修改对象的属性

  • 值对象接收者
func (p People) GetName() string {
	return p.Name
}
  • 无须修改状态的小对象或者固定值
  • 引用类型、字符串或者函数等
  • 调用时会按照其一个副本来执行调用
package main

import "fmt"

//定义结构体表示对象属性
type Student struct {
	Name string
	age   int
}

//对象的行为用方法表示
//func (方法接收者)方法名(参数) (返回值){return  值}
//值对象接收者

func(stu Student) SayHello(){
	//不可以修改对象的属性
	//stu.Name = "x86"
	fmt.Println("我是",stu.Name)
	fmt.Println("方法",stu)
}
func main(){
	stu := Student{"cloud",2}
	stu.SayHello()

	fmt.Println(stu)
	fmt.Println(stu.SayHello)
}

C:\Users\vSphere\go\src\day04>go run 01.go
我是 cloud
方法 {cloud 2}
{cloud 2}
0x49e310

  • 指针对象接收者
func (p *People) GetAge() uint {
	return p.Age
}

修改实例状态

大对象

调用时会按照实际值来执行调用

  • 指针对象接收者,可以修改对象属性
package main

import "fmt"

type Student struct {
	Name string
	age  int
}

// 指针对象接收者
func (stu *Student) SayHello() {
	//可以修改对象属性
	stu.Name = "法师"
}
func main() {
	stu := Student{"兵哥", 18}
	stu.SayHello()

	fmt.Println(stu)
	fmt.Println(stu.SayHello)
}
  • 结构体同名字段
type People struct {
	Name string `json:"name"`
	Age  uint   `json:"age"`
}
type Admin struct {
	People
	Name  string //会覆盖People中的Name
	Level uint
}
  • 调用方法
a := Admin{
	People: People{
		Name: "liLei",
		Age:  30,
	},
	Name:  "HanJie",
	Level: 3,
}
fmt.Println(a.Name)        //调用覆盖后的,打印值HanJie
fmt.Println(a.People.Name) //调用的子结构体内覆盖前的liLei

2.方法的声明

类型拥有的所有方法名都必须是唯一的,但不同的类型可以使用相同的方法名,比如Point和Path类型的Distance方法,也没有必须使用附加的字段来修饰方法名,比如PathDistance以示区别

使用方法的第一个好处,命名可以比函数更简短,在包的外部进行调用的时候,方法能够使用更加简短的名字且省略包的名字.

type Point struct {
	x, y float64
}
type Path []Point

func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.x-p.x, q.y-p.y)
}

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() {
	perim := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}
	fmt.Println(perim.Distance())
}

3.指针接受者的方法

由于主调函数会复制每一个实参变量,如果函数需要更新一个变量,或者如果一个实参太大而我们更希望避免复制整个实参,必须使用指针来传递变量的值,同样使用于更新接受者

绑定到指针类型,*Point

type Point struct {
	x, y float64
}

func (p *Point) ScaleBy(factor float64) {
	p.x *= factor
	p.y *= factor
}

func one() {
	r := &Point{1, 2}
	r.ScaleBy(2)
	fmt.Println(*r)
}
func two() {
	p := Point{1, 2}
	pptr := &p
	pptr.ScaleBy(2)
	fmt.Println(p)
}
func three() {
	p := Point{1, 2}
	(&p).ScaleBy(2)
	fmt.Println(p)
	p.ScaleBy(2)
	fmt.Println(p)
}

func main() {
	fmt.Println("one:")
	one()
	fmt.Println("two")
	two()
	fmt.Println("three")
	three()
}

实际上编译器会对变量进行&p的隐式转换,只有变量才允许这么做,包括结构体字段,像p.x和数组或者slice的元素,比如perim[0],不能够对一个不能取地址的Point接受者参数调用*Point方法,因为无法获取临时变量的地址

在合法的方法调用表达式中,只有符合下面三种形式的语句才能够成立

  • 实参接收者和形参接收者是同一个类型,比如都是T类型或者都是*T类型:
Point{1, 2}.Distance(q) //Point
pptr.ScaleBy(2)         //*Point
  • 实参接收者是T类型的变量而形参接收者是T类型,编译器会隐式地获取变量地址
p.ScaleBy(2) //隐式转换为(&p)
  • 实参接受者是*T类型而形参接收者是T类型,编译器会隐式地解引用接收者,获得实际的取值
pptr.Distance(q) //隐式转换为(*pptr)

如果所有类型T方法的接收者是T自己,而非*T,那么复制它的实例是安全的,调用方法的时候都必须进行一次vi复制,比如time.Duration的值作为实参传递到函数的时候就会复制.

但是任何方法的接收者是指针的情况下,应该避免复制T的实例,因为这么做可能会破坏内部原本的数据,比如复制bytes.Buffer实例只会得到相当于原来bytes数组的一个别名,随后的方法调用会产生不可预期的结果

4.nil是一个合法的接收者

方法的接收者允许nil指针作为实参

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

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

func main() {
	//!+main
	m := url.Values{"lang": {"en"}} // direct construction
	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"      (first value)
	fmt.Println(m["item"])     // "[1 2]"  (direct map access)

	m = nil
	fmt.Println(m.Get("item")) // ""
	m.Add("item", "3")         // panic: assignment to entry in nil map
	//!-main
}

5.通过结构体内嵌组成类型

内嵌可以更简单定义ColoredPoint类型,它包含Point类型的所有字段以及他更多的自有字段

type Point struct{ X, Y float64 }

type ColoredPoint struct {
	Point
	Color color.RGBA
}

//!-decl

func (p Point) Distance(q Point) float64 {
	dX := q.X - p.X
	dY := q.Y - p.Y
	return math.Sqrt(dX*dX + dY*dY)
}

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

func main() {
	//!+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"
	//!-main
}

/*
//!+error
	p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point
//!-error
*/

func init() {
	//!+methodexpr
	p := Point{1, 2}
	q := Point{4, 6}

	distance := Point.Distance   // method expression
	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)"
	//!-methodexpr
}

func init() {
	red := color.RGBA{255, 0, 0, 255}
	blue := color.RGBA{0, 0, 255, 255}

	//!+indirect
	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 and q now share the same Point
	p.ScaleBy(2)
	fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
	//!-indirect
}

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

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

内嵌的字段会告诉编译器生成额外的包装方法来调用Point声明的方法,这相当于以下代码:

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

匿名字段类型可以是个指向命名类型的指针,这个时候,字段和方法间接地来自于所指向的对象,这可以让我们共享通用的结构以及使对象之间的关系更加动态,多样化,

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 and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

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

type ColoredPoint struct {
	Point
	color.RGBA
}

那么这个类型的值可以拥有Point所有的方法和RGBA所有的方法,以及任何其他直接在coloredPoint类型中声明的方法

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

6.方法变量与表达式

我们可以在相同的表达式里使用和调用方法,也可以把两个操作分开

函数值需要提供实参而不需要提供接收者就能够调用

type Point struct{ X, Y float64 }

// traditional function
func Distance(p, q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}

p := Point{1,2}
q := Point{4,6}
distanceFromP := p.Distance   //方法变量
fmt.Println(distanceFromP(q))
var origin  Point
fmt.Println(distanceFromP(origin))

scaleP := p.ScaleBy  //方法变量

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

  • 与方法变量相关的是方法表达式

方法表达式和调用一个普通的函数不同,在调用方法的时候必须提供接收者,并且按照选择子的语法进行调用,而方法表达式写成T.f或者(*T).f,其中T是类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参,因此它可以像平常的函数一样调用

7.封装

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

封装:数据隐藏,是面向对象编成中重要的一方面

重点: 要封装一个对象,必须使用结构体

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

封装的3个优点

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

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

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

8.方法继承和重写

  • 继承

在结构体操作时,存在继承关系,子类可以使用父类的方法

package main

import "fmt"

type Person10 struct {
	name string
	age int
	sex string
}
type Student struct {
Person10
Score int
}

//父类方法 只能使用父类的结构体属性信息
func (p *Person10) Print() {
	fmt.Println(*p)
}
func main() {
stu := Student{Person10{"XX", 25, "男"}, 100}
//子类对象调用父类方法
stu.Print()
}

C:\Users\vSphere\go\src\day00>go run 16.go
{XX 25 男}


  • 方法继承
package main

import "fmt"

type People struct {
	Name string
	Age  int
}

type Student struct {
	People
	Socre int
}

func (p *People) SayHello() {
	fmt.Println(*p)
}

func (s *Student) SayBye() {
	fmt.Println(*s)

}

func main() {
	//创建stu对象
	stu := Student{People{"x86", 2}, 100}
	//子类可以调用父类的方法
	stu.SayHello()
	stu.SayBye()
}
  • 重写

如果子类(结构体)中的方法名与父类(结构体)中的方法名同名,在调用的时候是先调用子类(结构体)中的方法,这就方法的重写。

所谓的重写:就是子类(结构体)中的方法,将父类中的相同名称的方法的功能重新给改写。

package main

import "fmt"

type Person10 struct {
	name string
	age  int
	sex  string
}
type Student struct {
	Person10
	Score int
}

// 父类方法 只能使用父类的结构体属性信息
func (p *Person10) Print() {
	fmt.Println(*p)
}

// 子类方法
func (s *Student) Print() {
	fmt.Println(*s)
}
func main() {
	stu := Student{Person10{"XX", 25, "男"}, 100}
	//子类对象调用子类方法
	stu.Print()
}
  • 使用父类的方法
package main

import "fmt"

type Person1 struct {
	Name string
	Age int
}

type Student1 struct {
	Person1
	Score int
}

func (p *Person1)Print(){
	fmt.Println(p)
}
func (s *Student1)Print(){
	fmt.Println(*s)
}

func main(){
	stu := Student1{Person1{"x86",2},100}
	//采用就近原则 默认使用本结构体对应的方法
	stu.Print()
	//使用父类的方法
	stu.Person1.Print()
}
C:\Users\vSphere\go\src\day04>go run 03.go
{{x86 2} 100}
&{x86 2}
推荐阅读