go语言:结构体、接口

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) = 8
3 = 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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值