结构体
6.5 方法
Go语言中的方法(Method)是一种作用于特点类型变量的函数。这种特点类型的变量叫作接收器。
如果将特定类型理解为结构体或“类”时,接收器的概念就类似于其他语言中的 this或者self
Go 语言中,接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
6.5.1 为结构体添加方法
本节中,将会使用背包作为“对象”,将物品放入背包的过程作为“方法”,通过面向过程的方式和Go 结构体的方式来理解“方法”的概念。
-
面向过程实现方法
面向过程中没有“方法”概念,只能通过结构体和函数,由使用者使用函数参数和调用关系来形成接近“方法”的概念,代码如下:
type Bag struct { items []int } func Insert(b *Bag, itemid int) { b.items = append(b.items, itemid) } func main() { bag := new(Bag) Insert(bag, 100) }
-
Go语言的结构体方法
将背包及放入背包的物品中使用 Go 语言 构体和方法方式编写 :为*Bag 建一个方法,代码如下:
type Bag struct {
items []int
}
// b *Bag表示接收器,即Insert作用的对象实例
func (b *Bag) Insert itemid int) {
b.items = append(b.items, itemid)
}
func main() {
bag := new(Bag)
bag.Insert(100)
}
6.5.2 接收器—方法作用的目标
接收器的格式如下:
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数){ 函数体 }
-
接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如, Socket 类型的接收器变量应该命名为 s, Connector 类型的接收器变量应该命名为c等。
-
接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
-
方法名、参数列表、返回参数:格式与函数定义一致。
接收器根据接收器的类型可以分为指针接收器、非指针接收器。两种接收器在使用时会产生不同的效果。根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。
-
理解指针类型的接收器
指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的。
例子如下:
type Property struct { //定义属性结构体 val int } func (p *Property) SetValue(v int) { p.Val = v //修改p的成员变量 } func (p *Property) Value() { return p.value } func main() { p := new(Property) p.SetValue(100) //打印值 fmt.Println(p.Value()) //输出100 }
-
理解非指针类型的接收器
当方法作用于非指针接收器时, Go 语言会在代码运行时将接收器的值复制一份 。在非指针接收器的方法中可以获取接收器的成员值,但修改无效。
例子如下:
type Point struct {
X, Y int
}
func (p Point) Add(other Point) Point {
return Point{p.X + other.X, p.Y + other.Y}
}
func main() {
p1, p2 := Point{1,1}, Point{2,2}
p3 := p1.Add(p2)
fmt.Println(p3)
}
-
指针和非指针接收器的使用
计算机中,小对象由于值复制时的速度较快,适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制, 只是传递指针。
6.5.3 为类型添加方法
Go 语言可以对任何类型添加方法。给一种类型添加方法就像给结构体添加方法一样,因为结构体也是一种类型。
-
为基本类型添加方法
Go 语言中,使用type关键字可定义出新的自定义类型。之后就可以为自定义类型添加各种方法。我们习惯于使用面向过程的方式判断一个值是否为0, 例如:
if v = = 0 {
// v等于0
}
如果将 v当做整型对象,那么判断值就可以增加 lsZero() 方法,通过整个方法可以判断 v值是否0,例如:
if v.IsZero() {
// v等于0
}
详细实现流程请参考如下代码:
type MyInt int // 将int定义为MyInt类型 func (m MyInt) IsZero() bool { return m == 0 } func (m MyInt) Add(other int) int { return other + int(m) } func main() { var b MyInt fmt.Println(b.IsZero()) b = 1 fmt.Println(b.Add(2)) }
-
http包中的类型方法
Go语言中提供的http包里也大量使用了类型方法。Go语言使用http包进行HTTP的请求,使用http包的NewRequest()方法可以创建一个HTTP请求,填充请求中的http头(req.Header),再调用http.Client的Do包方法,将传入的HTTP请求发送出去。
下面代码演示创建一个HTTP请求:
func main() { client := &http.Client{} //创建一个HTTP请求 req, err := http,NewRequest("POST", "http://www.163.com", strings.NewReader("key=value")) //发现错误就打印并退出 if err != nil { fmt.Println(err) os.Exit(1) return } //为标头添加信息 req.Header.Add("User-Agent", "myClient") resp, err := client.Do(req) if err != nil { fmt.Println(err) os.Exit(1) return } //读取服务器返回的内容 data, err := ioutil.ReadAll(resp.Body) fmt.Println(string(data)) defer resp.Body.Close() }
为类型添加方法的过程是一个语言层特性,使用类型方法的代码经过编译器编译后的代码运行效率与传统的面向过程或面向对象的代码没有任何区别。因此,为了代码便于理解,在编码时使用 Go 语言的类型方法特性。
-
time包中的类型方法
Go语言提供的time包主要用于时间的获取和计算等。在这个包中,也使用了类型方法,例如:
func main() { fmt.Println(time.Second.String()) }
time.Second是一个常量,类型为Duration,而Duration实际是一个int64类型,定义如下:
type Duration int64
它拥有一个String的方法,部分定义如下:
func (d Duration) String() string { ... }
Duration.String()可以Duration 值转为字符串
-
6.6 类型内嵌和结构体内嵌
结构体运行其成员字段在声明时,没有字段名而只有类型,这种形式称为类型内嵌或匿名字段。
类型内嵌的写法如下:
type Data struct {
int
float32
bool
}
ins := &Data{
int: 10,
float32: 2.3,
bool: true,
}
类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而己,结构体要求字段名称必须唯一,因此 一个结构体中同种类型的匿名字段只能有一个。
结构体实例化后,如果匿名的字段类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌。
6.6.1 声明结构体内嵌
示例如下:
type BasicColor struct { //基础颜色
R,G,B float32 //红,绿,蓝三颜色分量
}
type Color struct { //完整颜色定义
BasicColor //基本颜色作为成员,只有类型没有字段名,写法叫作结构体内嵌
Alpha float32 //清晰度
}
func main() {
var c Color
//设置基本颜色分量
c.R = 1
c.G = 1
c.B = 0
c.Alpha = 1
fmt.Printf("%+v", c)
}
6.6.2 结构体内嵌特性
-
内嵌的结构体可以直接访问其成员变量
嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结
构体,结构体实例访问任一级的嵌入结构体成员时都只用给出字段名,而无须像传统结
构体字段一样,通过一层层的结构体字段访问到最终的字段。 例如, ins.a.b.c 的访问可以
简化为 ins.c
-
内嵌的结构体的字段名是它的类型名
内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名,代码如下:
var c Color
c.BasicColor.R = 1
c.BasicColor.G = 1
c.BasicColor.B = 0
6.6.3 使用组合思想描述对象特性
在面向对象思想中,实现对象关系需要使用“继承”特性。例如,人类不能飞行,鸟类可以飞行。人类和鸟类都可以继承自可行走类,但只有鸟类继承自飞行类。
面向对象的设计原则中也建议对象最好不要使用多重继承。
Go语言的结构体内嵌特性就是一种组合特性,使用组合特性可以快速构建对象的不同特性。
// 组合特性
type Flying struct{}
func (f *Flying) Fly() {
fmt.Println("can fly")
}
type Walkable struct{}
func (w *Walkable) Walk() {
fmt.Println("can walk")
}
type Human struct {
Walkable //人能行走
}
type Bird struct {
Walkable //鸟可以行走
Flying //鸟可以飞
}
func main() {
//实例化鸟类
b := new(Bird)
fmt.Println("Bird: ")
b.Fly()
b.Walk()
//实例化人类
h := new(Human)
fmt.Println("Human: ")
h.Walk()
}
使用Go语言的内嵌结构体实现对象特性,可以自由地在对象中增、删、改各种特性。Go 语言会在编译时检查能否使用这些特性。
6.6.4 初始化结构体内嵌
结构体内嵌初始化时,将结构体内嵌的类型作为字段名像普通结构体一样进行初始化,详细实现过程请参考如下代码 :
type Wheel struct {
Size int
}
type Engine struct {
Power int
Type string
}
type Car struct {
Wheel
Engine
}
func main() {
c := Car{
//初始化轮子
Wheel: Wheel{
Size: 18,
},
//初始化引擎
Engine: Engine {
Type: "1.4T",
Power: 143,
},
}
fmt.Printf("%+v\n", c)
}
6.6.5 初始化内嵌匿名结构体
在前面描述车辆和引擎的例子中,有时考虑编写代码的便利性,会将结构体直接定在嵌入的结构体中。 也就是说,结构体的定义不会被外部引用到。在初始化这个被嵌入的结构体时,就需要再次声明结构才能赋予数据。具体请参考如下代码:
type Wheel struct {
Size int
}
type Car struct {
Wheel
Engine struct { //Engine 结构体被直接定义在 Car 的结构体中
Power int
Type string
}
}
func main() {
c := Car{
Wheel: Wheel{
Size: 18,
},
Engine: struct { //初始化内嵌匿名结构体
Power int
Type string
}{
Type: "1.4T",
Power: 143,
},
}
fmt.Printf("%+v\n", c)
}