浅谈Go语言(7) - 接口与指针

1. 写在前面

  本节主要介绍接口和指针的基本概念与实现,到这个章节 Go 语言的数据类型介绍就全部完成了,本文是在2021年的春节期间写的,对作者来说还挺有意义,希望在2021年里大家都有个好的的开始。

2. 接口类型的使用

(1) 定义

  在Go语言中,谈到接口是指接口类型,和其他类型不一样的是接口无法被实例化,既不能用new也不能make函数创建出来。

  一般通过typeinterface声明接口类型,下面看声明接口的代码。

type BMW interface {
	SetName(name string)
	Name() string
	Category() string
}

  根据定义能看到接口类型的字面量和结构体的有些相似,结构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义。

(2) 实现

  只要一个数据类型的方法集合中有这 2 个方法,那么它就一定是BMW接口的实现类型。这是一种无侵入式的接口实现方式。这种方式还有一个专有名词,叫“Duck typing”,中文常译作“鸭子类型”,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

怎样判定一个数据类型的某一个方法实现的就是某个接口类型中的某个方法呢?

  1. 两个方法的签名需要完全一致
  2. 两个方法的名称要一模一样

  下面我们进行接口函数的实现

func (car *Car) SetName(name string) {
	car.name = name
}

func (car Car) Name() string {
	return car.name
}

func (car Car) Category() string {
	return "Luxury-Car"
}

(3) 使用

基本示例

  接下来我们通过实际代码来看如何使用接口类型

func main() {
	fmt.Println("----------------------------------------")
	car := Car{"bmw1181"}
	_, ok := interface{}(car).(BMW)
	fmt.Printf(" Car implements interface BMW: %v\n", ok)

	_, ok = interface{}(&car).(BMW)
	fmt.Printf("*Car implements interface BMW: %v\n", ok)

	fmt.Println("----------------------------------------")

	car.SetName("320iGT")
	fmt.Printf("This car is a %s, the name is %s.\n", car.Category(), car.Name())

	var bmw BMW = &car
	fmt.Printf("after [var bmw BMW = &car]: bmw's name is %s.\n", bmw.Name())

	fmt.Println("----------------------------------------")

	fmt.Printf("car's name is %s.\n", car.Name())
	bmw.SetName("525Li")
	fmt.Printf("after [bmw.SetName(\"525Li\")]: car's name is %s, bmw's name is %s.\n", car.Name(), bmw.Name())

	fmt.Println("----------------------------------------")

	fmt.Printf("car's name is %s.\n", car.Name())
	cartmp := car
	cartmp.name = "528Li"
	fmt.Printf("after [cartmp.name = \"528Li\"]: car's name is %s, cartmp's name is %s.\n", car.Name(), cartmp.Name())

	fmt.Println("----------------------------------------")
}

  大家可以先思考下运行的结果,再看实际输出结果是否与预想的一致

----------------------------------------
 Car implements interface BMW: false
*Car implements interface BMW: true
----------------------------------------
This car is a Luxury-Car, the name is 320iGT.
after [var bmw BMW = &car]: bmw's name is 320iGT.
----------------------------------------
car's name is 320iGT.
after [bmw.SetName("525Li")]: car's name is 525Li, bmw's name is 525Li.    
----------------------------------------
car's name is 525Li.
after [cartmp.name = "528Li"]: car's name is 525Li, cartmp's name is 528Li.
----------------------------------------

iface

  下面我们再看一段代码,并对代码进行分析

type Car struct {
	name string
}

type BMW interface {
	Name() string
}

func (car *Car) SetName(name string) {
	car.name = name
}

func (car Car) Name() string {
	return car.name
}

func main() {
	car := Car{"Mercedes-Benz"}
	var bmw BMW = car
	car.SetName("320iGT")
	fmt.Printf("car's name is %s, bmw's name is %s.\n", car.Name(), bmw.Name())
}

  由于SetName是指针方法,所以carSetName改变了carnamebmw的值没有改变是因为在var bmw BMW = car赋值的时候,只是拷贝了car值的副本。

  一个变量的值其实是这个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值。所以说,bmw的值与car的值肯定是不同的,无论是从它们存储的内容,还是存储的结构上来看都是如此。不过,我们可以认为,这时bmw的值中包含了car值的副本。这个专用的数据结构叫做iface,在 Go 语言的runtime包中就叫这个名字。

  iface的实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径等等。

(4) 扩展知识

接口变量值nil

  继续通过代码来进行讲解

func main() {
	var car1 *Car
	fmt.Println("The first car is nil.")
	car2 := car1
	fmt.Println("The second car is nil.")
	var bmw BMW = car2
	if bmw == nil {
		fmt.Println("The bmw is nil.")
	} else {
		fmt.Println("The bmw is not nil.")
	}
	fmt.Printf("The type of bmw is %T.\n", bmw)
	fmt.Printf("The type of bmw is %s.\n", reflect.TypeOf(bmw).String())
	fmt.Printf("The type of second car is %T.\n", car2)
}

  Go 语言会识别出赋予bmw的值是一个*Car类型的nil。然后 Go 语言就会用一个iface的实例包装它,包装后的产物就不是nil了。

  运行结果如下:

The first car is nil.
The second car is nil.
The bmw is not nil.
The type of bmw is *main.Car.       
The type of bmw is *main.Car.       
The type of second car is *main.Car.

接口间的组合

  接口类型间的嵌入也被称为接口的组合,上一章节的示例代码就用到了结构体间的嵌套。

  接口类型间的嵌入要更简单一些,因为它不会涉及方法间的“屏蔽”。只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译,即使同名方法的签名彼此不同也会是如此。

  下面简单使用代码进行简单展示:

type Car struct {
	name string
}

type Configure interface {
	Category() string
}

type BMW interface {
	Configure
	Name() string
}

func (car Car) Name() string {
	return car.name
}

func (car Car) Category() string {
	return "Ultimate"
}

func main() {
	car := Car{"320iGT"}
	var bmw BMW = &car
	fmt.Printf("This car is a %s, the name is %s.\n", bmw.Category(), bmw.Name())
}

// This car is a Ultimate, the name is 320iGT.

3. 指针

(1) 基本概念

  指针在Go语言中分为两个核心概念:

  • 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
  • 切片,由指向起始元素的原始指针、元素数量和容量组成。

  Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

  切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。

  一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。

(2) 定义与使用

  每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

ptr := &v    // v 的类型为 T

  其中v代表被取地址的变量,变量v的地址使用变量ptr进行接收,ptr的类型为*T,称做T的指针类型,*代表指针。

  在上面的接口部分的SetName函数中也使用了指针:

type Car struct {
	name string
}

func (car *Car) SetName(name string) {
	car.name = name
}

  下面再展示一段代码:

func main() {
	num1 := 1
	ptr := &num1 // 获取变量的指针
	fmt.Printf("num1:%d, ptr:%p\n", num1, ptr)

	num2 := *ptr // 将指针指向的num1的值赋给num2
	*ptr = 2 // 改变ptr指针指向地址的num1的值
	fmt.Printf("num1:%d, num2:%d\n", num1, num2)
}

  以上代码展示了如何进行变量的指针获取,同时如何通过指针获取指针指向的变量值,代码执行结果如下:

num1:1, ptr:0xc0000120a0
num1:2, num2:1

  打印指针类型

num1 := 1
ptr := &num1
fmt.Printf("ptr type: %T\n", ptr)

// ptr type: *int

(3) 使用指针修改值

  定义了2个swap函数,进行函数调用:

func swap1(a, b *int) {
	b, a = a, b
}

func swap2(a, b *int) {
	t := *a
	*a = *b
	*b = t
}

func main() {
	x, y := 1, 2
	fmt.Printf("x=%d, y=%d\n", x, y)

	swap1(&x, &y)
	fmt.Printf("run swap1 x=%d, y=%d\n", x, y)

	swap2(&x, &y)
	fmt.Printf("run swap2 x=%d, y=%d\n", x, y)
}

  执行结果:

x=1, y=2
run swap1 x=1, y=2
run swap2 x=2, y=1

(4) 指针类型转换

  下面分析一段指针类型转换的代码

type Car struct {
	name string
}

func main() {
	car := Car{"BMW320iGT"}
	carP := &car
	carPtr := uintptr(unsafe.Pointer(carP))
	fmt.Printf("%T, %T, %T\n", car, carP, carPtr)
}

// main.Car, *main.Car, uintptr

  以上代码使用了两个类型转换,先把carP转换成了一个unsafe.Pointer类型的值,然后紧接着又把后者转换成了一个uintptr的值,并把它赋给了变量carPtr。转换规则如下:

  • 一个指针值(比如*Car类型的值)可以被转换为一个unsafe.Pointer类型的值,反之亦然。
  • 一个uintptr类型的值也可以被转换为一个unsafe.Pointer类型的值,反之亦然。
  • 一个指针值无法被直接转换成一个uintptr类型的值,反之亦然。

  指针值转换成uintptr类型值的意义是什么呢,我们看下面的代码:

namePtr := carPtr + unsafe.Offsetof(carP.name)
nameP := (*string)(unsafe.Pointer(namePtr))
fmt.Printf("%s\n", *nameP)

// BMW320iGT

  unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位。

  实际上直接用取址表达式&(carP.name)就能拿到这个指针值,为何还要像以上代码中这样操作呢?当我们根本不知道这个结构体类型是什么的时候,就拿不到carP这个变量,也就无法获取name字段了。这样我们就可以直接修改埋藏得很深的内部数据了,当然不正确的改动会带来不可预知的问题甚至导致程序崩溃。

  对于指针主要掌握前3节就行,这一节实际上用得比较少,毕竟这样进行数据操作,如果操作不好会有很大的弊端。但是对于开发者而言,进行了解还是很有必要的。

参考文献:

  • 极客时间:Go语言核心36讲 by 郝林
  • http://c.biancheng.net/view/21.html

公众号推广

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小爱玄策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值