浅谈Go语言(7) - 接口与指针
文章目录
- 1. 写在前面
- 2. 接口类型的使用
- (1) 定义
- (2) 实现
- (3) 使用
- 基本示例
- iface
- (4) 扩展知识
- 接口变量值nil
- 接口间的组合
- 3. 指针
- (1) 基本概念
- (2) 定义与使用
- (3) 使用指针修改值
- (4) 指针类型转换
1. 写在前面
本节主要介绍接口和指针的基本概念与实现,到这个章节 Go 语言的数据类型介绍就全部完成了,本文是在2021年的春节期间写的,对作者来说还挺有意义,希望在2021年里大家都有个好的的开始。
2. 接口类型的使用
(1) 定义
在Go语言中,谈到接口是指接口类型,和其他类型不一样的是接口无法被实例化,既不能用new
也不能make
函数创建出来。
一般通过type
和interface
声明接口类型,下面看声明接口的代码。
type BMW interface {SetName(name string)Name() stringCategory() string
}
根据定义能看到接口类型的字面量和结构体的有些相似,结构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义。
(2) 实现
只要一个数据类型的方法集合中有这 2 个方法,那么它就一定是BMW接口的实现类型。这是一种无侵入式的接口实现方式。这种方式还有一个专有名词,叫“Duck typing”,中文常译作“鸭子类型”,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。
怎样判定一个数据类型的某一个方法实现的就是某个接口类型中的某个方法呢?
- 两个方法的签名需要完全一致
- 两个方法的名称要一模一样
下面我们进行接口函数的实现
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 = &carfmt.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 := carcartmp.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 = carcar.SetName("320iGT")fmt.Printf("car's name is %s, bmw's name is %s.\n", car.Name(), bmw.Name())
}
由于SetName
是指针方法,所以car
的SetName
改变了car
的name
,bmw
的值没有改变是因为在var bmw BMW = car
赋值的时候,只是拷贝了car
值的副本。
一个变量的值其实是这个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值。所以说,bmw
的值与car
的值肯定是不同的,无论是从它们存储的内容,还是存储的结构上来看都是如此。不过,我们可以认为,这时bmw
的值中包含了car
值的副本。这个专用的数据结构叫做iface
,在 Go 语言的runtime
包中就叫这个名字。
iface
的实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径等等。
(4) 扩展知识
接口变量值nil
继续通过代码来进行讲解
func main() {var car1 *Carfmt.Println("The first car is nil.")car2 := car1fmt.Println("The second car is nil.")var bmw BMW = car2if 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 {ConfigureName() string
}func (car Car) Name() string {return car.name
}func (car Car) Category() string {return "Ultimate"
}func main() {car := Car{"320iGT"}var bmw BMW = &carfmt.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 := 1ptr := &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, 2fmt.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 := &carcarPtr := 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
浅谈Go语言(7) - 接口与指针相关推荐
- c语言指针很危险,浅谈C语言中指针使用不当的危险性.doc
浅谈C语言中指针使用不当的危险性.doc 第 19 卷 Vol . 19 第 2 期 No . 2 洛阳师专学报 Journal of Luoyang Teachers College 2000 年 ...
- c语言弱符号与函数指针,浅谈C语言中的强符号、弱符号、强引用和弱引用【转】...
首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引用.在看到3.5.5节弱符号和强符号时,我感觉有些困惑,所以写下此篇,希望能和同样 ...
- c语言 去掉双引号_技术分享|浅谈C语言陷阱和缺陷
良好的软件架构.清晰的代码结构.掌握硬件.深入理解C语言是防错的要点,人的思维和经验积累对软件可靠性有很大影响.C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步.软件的质量是由程 ...
- c语言乐学编程作业答案,信息乐学|浅谈C语言
原标题:信息乐学|浅谈C语言 一大波C语言的干货正在靠近 刚刚成为大学生的小萌新们,经过两个多月的学习,你们对大学的多彩生活是否还满意?全新的学习方式你们是否还适应?然而,新鲜劲还没过,第一件让你们头 ...
- 浅谈C语言内存(栈)
浅谈C语言内存 文章目录 浅谈C语言内存 内存分配 栈 斐波纳契数列 内存分配 在C语言中内存分别分为栈区(stack).堆区(heap).未初始化全局数据区.已初始化全局数据区.静态常量区(stat ...
- c语言函数调用参数调用的太少,浅谈C语言函数调用参数压栈的相关问题
参数入栈的顺序 以前在面试中被人问到这样的问题,函数调用的时候,参数入栈的顺序是从左向右,还是从右向左.参数的入栈顺序主要看调用方式,一般来说,__cdecl 和__stdcall 都是参数从右到左入 ...
- c程序语言的常量变量和标识符,浅谈C语言中的常量与变量.pdf
课程教育研究 CourseEducationResearch 2014年4月 上旬刊 教学.信息 浅谈C语言中的常量与变量 刘 星 (青 岛工学院商学院 山东 青岛 266300) [摘要]在任何一种 ...
- c语言结构共用体的作用,浅谈C语言共用体和与结构体的区别
共用体与结构体的区别 共用体: 使用union 关键字 共用体内存长度是内部最长的数据类型的长度. 共用体的地址和内部各成员变量的地址都是同一个地址 结构体大小: 结构体内部的成员,大小等于最后一个成 ...
- 浅谈go语言交叉编译
浅谈go语言交叉编译 基础 cgo cgo设置编译和链接参数 静态库和动态库 静态库 动态库 静态编译 cgo的内部连接和外部连接 internal linking external linking ...
最新文章
- C/S架构程序多种类服务器之间实现单点登录(转)
- 服务器硬件电路设计书籍,家庭网关硬件接口电路设计大全——电路精选(3)...
- 带你读懂Spring Bean 的生命周期,嘿,就是玩儿~
- 在mysql中什么情况下不能指定字符集_如何为Mysql选择合适的字符集?
- python中的数组按顺序切片_python切片(获取一个子列表(数组))详解
- linux 文件怎么不让删,请问如何设置权限,可以禁止用户删除文件
- jquery-超好用的数据容器-data方法
- CISCO无线AP配置手册
- 计算机网络数据链路层之扩展以太网(含以太网交换机及虚拟局域网)
- navicat 不能正常启动
- shell脚本:一次读取文件的一行,并输出
- 中国银保监会公布银行业金融机构目前共有4608家(附全部名录)
- 21天学通c语言中用的编译器,21天学通C语言
- 如何隐藏C/C++编译生成的函数符号
- YXC | 蓝牙耳机晶振怎么选?
- 期货的暴富逻辑是什么?
- 【老九学堂】【C语言进阶】递归调用
- 数学:它的内容、方法、意义的目录
- 读书笔记 - 《天局》
- 16进制转10进制 nodejs_Js字符串与十六进制的相互转换 【转】