1. 结构体:
前面学习的数组、字典、字符串等数据类型都是 Go语言的内置数据类型,struct结构体是用户自定义数据类型,用户可以使用其他基础类型构造出需要的结构体类型。
1.1 结构体变量的创建:
创建一个结构体变量有多种形式:
(1)“KV形式”:这种创建形式可以只指定部分字段的初值,也可以一个字段都不指定,那些没有指定初值的字段会自动初始化为相应类型的零值;
(2)“顺序形式”:这种创建形式在初始化结构体时不指定字段名,但必须提供所有字段的初值。
package main
import "fmt"
//定义结构体类型:
type Circle struct {
x int
y int
}
func main() {
//KV形式 创建:
var c Circle = Circle{
x:10,
y:20,
}
fmt.Println(c)
fmt.Println(c.x, c.y)
//顺序形式创建:
var d Circle = Circle {30, 40}
fmt.Println(d)
fmt.Println(d.x, d.y)
}
------
{10 20}
10 20
{30 40}
30 40
1.2 零值结构体和nil结构体:
nil结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用1个指针的存储空间,也就是一个机器字的内存大小(32位机器占4字节,64位机器占8字节)。
而零值结构体是会实实在在的占用内存空间的,只不过每个字段都是零值。如果结构体里面字段非常多,那么占用的内存空间也会很大。
Go语言的 unsafe
包提供了获取结构体占用内存大小的函数 Sizeof()。
package main
import (
"fmt"
"unsafe"
)
type Circle struct {
x int
y int
z int
}
func main() {
var c Circle = Circle{} //零值结构体,结构体中所有元素值均为零
var d Circle //零值结构体
fmt.Println(unsafe.Sizeof(c))
fmt.Println(unsafe.Sizeof(d))
var p *Circle = nil //nil结构体
fmt.Println(unsafe.Sizeof(p))
p = new(Circle)
fmt.Println(unsafe.Sizeof(p))
}
------
24
24
8
8
1.3 结构体的拷贝:
结构体之间可以相互拷贝,本质上是 深拷贝 操作,拷贝了结构体内部的所有字段。
package main
import "fmt"
type Circle struct {
x int
y int
z int
}
func main() {
var c Circle = Circle {1, 2, 3}
var d Circle = c //拷贝
d.x = 4
d.y = 5
d.z = 6
fmt.Println(c, d)
}
------
{1 2 3} {4 5 6}
1.4 无处不在的结构体:
通过观察Go语言的底层源码,可以发现所有的Go语言的内置的高级数据结构都是由结构体来完成的。
切片的头实际上是一个结构体类型,字符串的头也是结构体类型,以及字典头,都是结构体类型:
//slice 切片头:
type slice struct {
array unsafe.Pointer //unsafe.Pointer 指针类型
len int
cap int
}
//字符串头:
type string struct {
array unsafe.Pointer //底层字节数组的地址
len int
}
//字典头:
type hmap struct {
count int
...
buckets unsafe.Pointer //hash桶地址
...
}
1.5 结构体中的数组和切片:
理解 数组与切片在内存形式上的区别:
数组只有“体”,切片除了“体”之外,还有“头”部。切片的头部和内容体是分离的,使用指针关联起来。
通过下面的示例程序可以看到二者在内存形式的区别:(单独体 与 头体分离)
package main
import (
"fmt"
"unsafe"
)
type ArrayStruct struct {
value [10]int
}
type SliceStruct struct {
value []int
}
func main() {
var as ArrayStruct = ArrayStruct{[...]int{0,1,2,3,4,5,6,7,8,9}}
var ss SliceStruct = SliceStruct{[]int{0,1,2,3,4,5,6,7,8,9}}
fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))
}
------
80 24
解析:
unsafe.Sizeof(as) = 80, 是数组体本身的10个整型元素占用的内存大小:810=80;
unsafe.Sizeof(ss) = 24, 是切片“头”结构体占用的内存大小:sizeof(pointer + len + cap) = 83 = 24。
注意:
使用 unsafe.Sizeof()
函数计算一个切片占用的内存大小时,求得的结果只包含切片头的大小,不包含切片体的占用内存的大小。
1.6 结构体方法:
Go语言通过“方法”来支持面向对象编程(OOP)。
《Go编程设计语言》对面向对象的解释:
对象就是简单的一个值或者变量,并且拥有其方法,而方法是某种特定类型的函数。面向对象编程就是使用方法来描述每个数据结构的属性和操作,于是,使用者不需要了解对象本身的实现。
方法声明举例:
type Point struct { X, Y float64 }
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X - p.X, q.Y - p.Y)
}
方法的声明 与 普通函数的声明类似,区别在于方法的声明在函数名字前面多了一个参数(例中的 (p Point)
)。这个参数的作用是 把这个方法绑定到这个参数对应的类型上。 (“方法”必须有一个它所绑定的类型,就像是C++的类的成员函数必须归属于某个指定的类。)
其中,附加的参数 p
称为方法的 “接收者”,它源自早先的面向对象的编程语言,用来描述主调方法就像 向对象发送消息,类似于C++中的 this 指针,用于指定调用方法的对象。Go语言中接收者不使用特殊名(比如this或者self),而是由程序员自己选择名字,因此一般选择简短且在整个方法名中始终保持一致的名字,最常用的就是取类型名称的首字母,如 Point 中的 p。
package main
import "fmt"
type Circle struct {
x int
y int
z int
}
func (c Circle) Area() float64 {
return float64(c.x) * float64(c.y) * float64(c.z)
}
func (c Circle) Circumference() float64 {
return float64(c.x) + float64(c.y) + float64(c.z)
}
func main() {
var c Circle = Circle {
x:2,
y:3,
z:4,
}
fmt.Println(c.Area(), c.Circumference())
var pc *Circle = &c
fmt.Println(pc.Area(), pc.Circumference())
}
------
24 9
24 9
结构体的值类型 和 指针类型访问内部字段和方法在形式上是一样的。这点不同于C++语言,在C++中值访问使用“.”,指针访问使用“->”操作符。
1.7 Go语言的结构体没有多态性:
Go语言的结构体不支持多态,多态是指父类定义的方法可以调用子类的=实现的方法,不同的子类有不同的实现,从而给父类的方法带来了多样的不同行为。
Go语言的结构体明确不支持这种形式的多态,外部结构体的方法不能覆盖内部结构体的方法。面向对象的多态性需要通过Go语言的 接口 特性来模拟。
2. 接口:
Go语言中的接口定义使用 interface
关键字,定义形式与结构体类似。
Go语言的接口是隐式的,只要结构体上定义的“方法”在形式上(名称、参数、返回值)和 接口定义的“方法”一样,那么这个结构体就自动实现了这个接口,我们就可以使用这个接口变量来指向这个结构体对象。
package main
import "fmt"
type Smellable interface {
smell() //接口Smellable中定义的方法 smell()
}
type Eatable interface {
eat()
}
type Apple struct {}
type Flower struct {}
func (a Apple) smell() { //结构体Apple定义的方法 smell()
fmt.Println("apple can smell")
}
func (a Apple) eat() {
fmt.Println("apple can eat")
}
func (f Flower) smell() { //结构体Flower定义的方法 smell()
fmt.Println("flower can smell")
}
func main() {
var s1 Smellable //接口类型的变量s1
var s2 Eatable
var apple Apple = Apple{} //结构体类型的变量 apple
var flower Flower = Flower{}
s1 = apple //"接口 = 结构体"
s1.smell() //=apple.smell() //显式调用接口的方法,就相当于调用结构体的方法
s1 = flower
s1.smell() //=flower.smell()
s2 = apple
s2.eat()
var f Flower = flower
f.smell() //正常形式的调用结构体方法
}
------
apple can smell
flower can smell
apple can eat
flower can smell
2.1 空接口:
如果一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式的实现了空接口。
Go语言为了避免用户重复定义很多空接口,它自己内置了空接口:interface{}
空接口里面没有方法,因此不具备任何能力,但是它的作用在于 可以容纳任意对象,它是一个万能容器。例如实现一个字典,字典的key是字符串,但是希望value可以容纳 任意类型的对象,这时就可以使用空接口来实现:
package main
import "fmt"
func main() {
var user map[string]interface{} = map[string]interface{} {
"age": 30,
"address": "Shenzhen Nanshan",
"married": true,
} //注意这个map中每个元素的value字段类型都不一样
fmt.Println(user)
var age int = user["age"].(int) //注意interface{} 的类型转换写法
var address string = user["address"].(string)
var married bool = user["married"].(bool)
fmt.Println(age, address, married)
}
------
map[address:Shenzhen Nanshan age:30 married:true]
30 Shenzhen Nanshan true
上个例子中 user字典变量的类型是 map[string]interface{} ,从这个字典中直接读取得到的 value类型是 interface{} ,需要通过类型转换才能得到期望的变量。
2.2 用接口来模拟多态:
接口是一种特殊的容器,它可以容纳多种不同的对象,只要这些对象都同样实现了接口定义的方法。
package main
import "fmt"
type Fruitable interface {
eat()
}
type Fruit struct {
Name string
Fruitable //匿名内嵌接口变量
}
func (f Fruit) want() {
fmt.Printf("I like")
f.eat() //外部结构会自动继承匿名内嵌变量的方法
}
type Apple struct {}
func (a Apple) eat() {
fmt.Println("eating apple")
}
type Banana struct {}
func (b Banana) eat() {
fmt.Println("eating banana")
}
func main() {
var f1 = Fruit{"Apple", Apple{}}
var f2 = Fruit{"Banana", Banana{}}
f1.want()
f2.want()
}
------
I like eating apple
I like eating banana
使用这种方式模拟多态 本质上是通过组合属性变量(Name)和接口变量(Fruitable)来做到的,属性变量时对象的数据,而接口变量是对象的功能,将它们组合到一起就形成了一个完整的多态性的结构体。
参考内容:
https://zhuanlan.zhihu.com/p/50654803
https://zhuanlan.zhihu.com/p/50942676