浅谈Go语言(6) - 函数与结构体
文章目录
- 1. 写在前面
- 2. 函数的使用
- (1) 函数签名
- (2) 高阶函数
- 函数作为参数传入
- 函数作为结果返回
- 闭包
- 传入函数的参数值情况
- 3. 结构体
- (1) 定义
- (2) 嵌入字段
- (3) 值方法和指针方法
1. 写在前面
本章节我们介绍 Go 语言的模块化编程,包括几个重要的数据类型以及一些模块化编程的技巧。
2. 函数的使用
在 Go 语言中,函数是一等的(first-class)公民,函数类型也是一等的数据类型。这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。
函数值可以由此成为能够被随意传播的独立逻辑组件(或者说功能模块)。对于函数类型来说,它是一种对一组输入、输出进行模板化的重要工具,它比接口类型更加轻巧、灵活,它的值也变成了可被热替换的逻辑组件。
(1) 函数签名
我们先看一段代码
package mainimport "fmt"type Printer func(contents string) (n int, err error)func printToStd(contents string) (bytesNum int, err error) {return fmt.Println(contents)
}func main() {var p Printerp = printToStdp("something")
}
代码中声明了一个函数类型Printer
,声明的右边是关键字func
,在func右边的就是这个函数类型的参数列表和结果列表。其中,参数列表必须由圆括号包裹,而只要结果列表中只有一个结果声明,并且没有为它命名,我们就可以省略掉外围的圆括号。
书写函数签名的方式与函数声明基本一致,只是函数名称和func
互换了位置。
只要两个函数的参数列表和结果列表中的元素顺序及其类型是一致的,它们就是一样的函数,或者说是实现了同一个函数类型的函数。
函数Printer
的签名与printToStd
的是一致的,因此printToStd
是Printer
的一个实现,即使它们的名称以及有的结果名称是不同的。
现在再回去看代码里面main
函数的定义和执行,是不是更加的清晰了。
(2) 高阶函数
高阶函数也是函数式编程中的重要概念和特征,高阶函数满足以下的两个条件之一。
- 接受其他的函数作为参数传入
- 把其他的函数作为结果返回
函数作为参数传入
我们先看代码:
package mainimport ("errors""fmt"
)type operate func(x, y int) intfunc calculate(x int, y int, op operate) (int, error) {if op == nil {return 0, errors.New("invalid operation")}return op(x, y), nil
}func main() {add := func(x, y int) int {return x + y}sub := func(x, y int) int {return x - y}x, y := 10, 12resultAdd, _ := calculate(x, y, add)fmt.Printf("calculate add(%v, %v) --> %v\n", x, y, resultAdd)a, b := 12, 10resultSub, _ := calculate(a, b, sub)fmt.Printf("calculate sub(%v, %v) --> %v\n", a, b, resultSub)
}// go run result:
// calculate add(10, 12) --> 22
// calculate sub(12, 10) --> 2
以上代码中calculate
函数就是一个高阶函数,calculate
函数中先用卫述语句检查一下参数,如果operate
类型的参数op
为nil
,那么就直接返回0
和一个代表了具体错误的error
类型值。
卫述语句是指被用来检查关键的先决条件的合法性,并在检查未通过的情况下立即终止当前代码块执行的语句。在 Go 语言中,if 语句常被作为卫述语句。
我们在main
函数中传入一个operate类型的函数值,这个函数值应该怎么写?只要它的签名与operate类型的签名一致,并且实现得当就可以了,所以在add
里面实现一个加法函数。
函数作为结果返回
下面我们来看一段使用函数作为结果返回的代码:
package mainimport ("errors""fmt"
)type operate func(x, y int) inttype calculateFunc func(x int, y int) (int, error)func genCalculator(op operate) calculateFunc {return func(x int, y int) (int, error) {if op == nil {return 0, errors.New("invalid operation")}return op(x, y), nil}
}func main() {opMultiply := func(x, y int) int {return x * y}x, y := 2, 3multiply := genCalculator(opMultiply)result, _ := multiply(x, y)fmt.Printf("calculate multiply(%v, %v) --> %v\n", x, y, result)
}// go run result:
// calculate multiply(2, 3) --> 6
genCalculator
函数的唯一结果的类型就是calculateFunc
闭包
闭包专业术语叫自由变量,在一个函数中存在对外来标识符的引用,外来标识符既不代表当前函数的任何参数或结果,也不是函数内部声明的,它是直接从外边拿过来的。
闭包函数就是因为引用了自由变量,而呈现出了一种“不确定”的状态,也叫“开放”状态。它的内部逻辑并不是完整的,有一部分逻辑需要这个自由变量参与完成,而后者到底代表了什么在闭包函数被定义的时候却是未知的。
代码中的genCalculator
函数内部,就实现了一个闭包
func genCalculator(op operate) calculateFunc {return func(x int, y int) (int, error) {if op == nil {return 0, errors.New("invalid operation")}return op(x, y), nil}
}
genCalculator
函数只做了一件事,那就是定义一个匿名的、calculateFunc
类型的函数并把它作为结果值返回。
这个匿名的函数就是一个闭包函数。它里面使用的变量op
既不代表它的任何参数或结果也不是它自己声明的,而是定义它的genCalculator
函数的参数,所以是一个自由变量。这个自由变量究竟代表了什么,这一点并不是在定义这个闭包函数的时候确定的,而是在genCalculator
函数被调用的时候确定的。
Go 语言编译器读到if op == nil {
这里时会试图去寻找op
所代表的东西,它会发现op
代表的是genCalculator
函数的参数,然后,它会把这两者联系起来。这时可以说,自由变量op
被“捕获”了。当程序运行到这里的时候,op
就是那个参数值了。如此一来,这个闭包函数的状态就由“不确定”变为了“确定”,或者说转到了“闭合”状态,至此也就真正地形成了一个闭包。
实现闭包的意义是在动态生成那部分程序的逻辑,这与 GoF
设计模式中的“模板方法”模式有着异曲同工之妙。
传入函数的参数值情况
我们继续看一段代码:
package mainimport "fmt"func main() {array1 := [3]string{"a", "b", "c"}fmt.Printf("The array: %v\n", array1)array2 := modifyArray(array1)fmt.Printf("The modified array: %v\n", array2)fmt.Printf("The original array: %v\n", array1)
}func modifyArray(a [3]string) [3]string {a[1] = "x"return a
}
输出结果为:
The array: [a b c]
The modified array: [a x c]
The original array: [a b c]
根据以上代码的执行结果,能看出传给函数的参数值都会被复制,函数在其内部使用的并不是参数值的原值,而是它的副本。
由于数组是值类型,所以每一次复制都会拷贝它,以及它的所有元素值。在modify
函数中修改的只是原数组的副本而已,并不会对原数组造成任何影响。
对于引用类型,比如:切片、字典、通道,像上面那样复制它们的值,只会拷贝它们本身而已,并不会拷贝它们引用的底层数据。以切片值为例,如此复制的时候,只是拷贝了它指向底层数组中某一个元素的指针,以及它的长度值和容量值,而它的底层数组并不会被拷贝。
就算我们传入函数的是一个值类型的参数值,但如果这个参数值中的某个元素是引用类型的,那就需要注意了。
package mainimport "fmt"func main() {complexArray1 := [3][]string{[]string{"d", "e", "f"},[]string{"g", "h", "i"},[]string{"j", "k", "l"},}fmt.Printf("The complexArray1: %v\n", complexArray1)complexArray2 := modifyArray(complexArray1)fmt.Printf("The modified complexArray2: %v\n", complexArray2)fmt.Printf("The original complexArray1: %v\n", complexArray1)}func modifyArray(a [3][]string) [3][]string {a[1][1] = "x"return a
}
以上代码运行结果:
The complexArray1: [[d e f] [g h i] [j k l]]
The modified complexArray2: [[d e f] [g x i] [j k l]]
The original complexArray1: [[d e f] [g x i] [j k l]]
3. 结构体
(1) 定义
通过代码直接看结构体的定义
// Building 代表建筑的基本信息
type Building struct {area float32 // 面积roomnum int // 房间数
}func (bd Building) String() string {return fmt.Sprintf("area:%f, roomnum:%d", bd.area, bd.roomnum)
}
上面代码中的String
方法的功能是提供当前值的字符串表示形式。
结构体的使用:
func main() {building := Building{area: 100, roomnum: 5}fmt.Printf("The building: {%s}\n", building)
}// The building: {area:100.000000, roomnum:5}
知识点:在 Go 语言中,我们可以通过为一个类型编写名为String
的方法,来自定义该类型的字符串表示形式。这个String
方法不需要任何参数声明,但需要有一个string
类型的结果声明。方法隶属的类型其实并不局限于结构体类型,但必须是某个自定义的数据类型,并且不能是任何接口类型。
(2) 嵌入字段
继续看如下代码:
type House struct {name string // 名称Building // 建筑的基本信息
}func (hs House) String() string {return fmt.Sprintf("name:%s, %s", hs.name, hs.Building.String())
}
能够发现另一个字段声明中只有Building
,字段声明Building
代表了House
类型的一个嵌入字段。Go 语言规范规定,如果一个字段的声明中只有字段的类型名而没有字段的名称,那么它就是一个嵌入字段,也可以被称为匿名字段。我们可以通过此类型变量的名称后跟“.”,再后跟嵌入字段类型的方式引用到该字段。也就是说,嵌入字段的类型既是类型也是名称。
下面我们使用house
来定义
func main() {building := Building{area: 100, roomnum: 5}fmt.Printf("The building: {%s}\n", building)house := House{name: "apartment",Building: building,}fmt.Printf("The house: {%s}\n", house)
}
Go 语言中没有继承的概念,只有类型间的组合,具体的原因可见Go语言官网。
(3) 值方法和指针方法
上面看到的函数都是值方法,下面我们看一段指针方法:
func (hs *House) SetName(name string) {hs.name = name
}
调用指针方法后,输出结果:
func main() {building := Building{area: 100, roomnum: 5}fmt.Printf("The building: {%s}\n", building)house := House{name: "apartment",Building: building,}fmt.Printf("The house: {%s}\n", house)house.SetName("residence")fmt.Printf("The house: {%s}\n", house)
}// The building: {area:100.000000, roomnum:5}
// The house: {name:apartment, area:100.000000, roomnum:5}
// The house: {name:residence, area:100.000000, roomnum:5}
根据以上事例,值方法和指针方法的区别已经比较明显了。
值方法的接收者是该方法所属的那个类型值的一个副本。我们在该方法内对该副本的修改一般都不会体现在原值上,除非这个类型本身是某个引用类型(比如切片或字典)的别名类型。而指针方法的接收者,是该方法所属的那个基本类型值的指针值的一个副本。我们在这样的方法内对该副本指向的值进行修改,就会修改原值。
参考文献:
- 极客时间:Go语言核心36讲 by 郝林
- Go语言官网:https://golang.org/doc/faq#inheritance
浅谈Go语言(6) - 函数与结构体相关推荐
- c语言 一个函数返回结构体指针,详解C语言结构体中的函数指针
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合.所以,标准C中的结构体是不允许包含成员函数的,当然C++中的结构体对此进行了扩展.那么,我们在C语言的结构体中,只能通过定义函数指针的方式, ...
- c语言 返回函数是结构体指针变量,一个函数返回值为指向结构体的指针的问题...
一个函数返回值为指向结构体的指针的问题 #include #include struct student { int num; char name[10]; struct student *next; ...
- c语言sscanf函数和结构体,C语言sprintf与sscanf函数 -电脑资料
1.前言 我们经常涉及到数字与字符串之间的转换,例如将32位无符号整数的ip地址转换为点分十进制的ip地址字符串,或者反过来,总结一下.C语言提供了一些列的格式化输入输出函数,最基本的是面向控制台标准 ...
- c语言弱符号与函数指针,浅谈C语言中的强符号、弱符号、强引用和弱引用【转】...
首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引用.在看到3.5.5节弱符号和强符号时,我感觉有些困惑,所以写下此篇,希望能和同样 ...
- c语言结构共用体的作用,浅谈C语言共用体和与结构体的区别
共用体与结构体的区别 共用体: 使用union 关键字 共用体内存长度是内部最长的数据类型的长度. 共用体的地址和内部各成员变量的地址都是同一个地址 结构体大小: 结构体内部的成员,大小等于最后一个成 ...
- c语言函数参数压栈,函数调用压栈 浅谈C语言函数调用参数压栈的相关问题
想了解浅谈C语言函数调用参数压栈的相关问题的相关内容吗,在本文为您仔细讲解函数调用压栈的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:函数调用压栈,下面大家一起来学习吧. 参数入栈的顺序 ...
- c语言 去掉双引号_技术分享|浅谈C语言陷阱和缺陷
良好的软件架构.清晰的代码结构.掌握硬件.深入理解C语言是防错的要点,人的思维和经验积累对软件可靠性有很大影响.C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步.软件的质量是由程 ...
- 浅谈go语言交叉编译
浅谈go语言交叉编译 基础 cgo cgo设置编译和链接参数 静态库和动态库 静态库 动态库 静态编译 cgo的内部连接和外部连接 internal linking external linking ...
- c语言乐学编程作业答案,信息乐学|浅谈C语言
原标题:信息乐学|浅谈C语言 一大波C语言的干货正在靠近 刚刚成为大学生的小萌新们,经过两个多月的学习,你们对大学的多彩生活是否还满意?全新的学习方式你们是否还适应?然而,新鲜劲还没过,第一件让你们头 ...
最新文章
- thread.sleep是让哪个线程休眠_java开发两年,这些线程知识你都不知道,你怎么涨薪?...
- 变速积分pid控制器matlab,变速积分PID控制系统设计.docx
- 日计不足涓滴成河-自定义响应结果格式化器
- PTA 数据结构与算法题目集(中文)
- 支持向量机SVM算法原理及应用(R)
- 使用Eclipse查看反编译后的代码(Decompiler 插件)
- easyUI 动态参数名称和动态参数值
- threejs 加载obj模型
- Vant组件库 引入 阿里矢量图 添加自己喜欢的 ICON
- VMware-Esxi6.7各个版本镜像文件iso下载链接
- 三款适合HDMI信号分配的分配器芯片
- 从零开始开发微信小程序(四):微信小程序绑定系统账号并授权登录之后台端...
- 搞个服务器安装黑群晖系统,牛人闲置电脑大改造!超低成本组建家用黑群晖NAS...
- 腾讯云Ubuntu建FTP心得
- ExtJS教程(5)---Ext.data.Model之高级应用
- 《数字经济2.0:引爆大数据生态红利》
- Gait Part论文阅读笔记
- 最全的免费OA试用地址
- 25种用WordPress博客在网上赚钱的方法
- 支付宝发的计算机,支付宝电脑网站支付接口如何使用?