
方法
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}