欢迎访问我的博客,你的支持,是我最大的动力!

Go语言学习笔记(2)-函数/复合数据类型

Linux 小马奔腾 1306℃ 评论
目录:
[显示]

函数

函数没有接收者
方法有接收者,方法属于结构体、类型

函数基础

Golang 函数不支持嵌套 nested 、重载 overload、默认参数 default parameter

函数声明结构式:

func funcName(input1 type1,input2 type2)(output type1,output type2) {
......// 逻辑代码
......return value1,value2
}

声明一个在外部定义的函数只需给出函数名与函数签名,不需要写出函数体

func hello(str,num int)  //外部实现

函数也可以通过声明的方式做为函数类型被使用

type addNum func(int,int) int

函数值之间可以相互比较,若引用相同的函数或返回值都是 nil 则认为它们是相同的函数

函数的返回值

若有返回值,必须在函数最后添加 return 语句
return 之后的语句都不会执行

多返回值

若返回值流量变量名,则可以只有一个 return 而不必具体指出要返回的变量是什么,如

函数作为参数

函数是第一类对象,可作为参数传递
被调用函数返回值个数、返回值类型和反回值顺序需要与调用函数所需要的实参保持一致

func f1(a,b,c int)
func f2(a,b int)(int,int,int)
f1(f2(a,b))   //f2函数作为参数传递给f1

函数类型

所有拥有相同参数与返回值,是一种函数类型
函数作为类型好处是可以把这个类型的函数当做值来传递

type typeName func(input1 inputType1,input2 inputType2 [, ...])(result1 resultType1 [, ... ])

在写一些通用接口时,把函数作为值和类型非常有用,可以使得程序变得非常灵活

可变参数
func myfunc(arg ...int){ ... }

函数中最后一个参数是 ...type 形式,可变参数长度可以是0个,可变参数部分类似一个切片,可在函数内部被迭代操作

下面例子中,注意f2和f3的参数和调用方式
类型 ...type 本质上是一个切片,即 []type

可变参数要求必须是相同类型,如果是不同类型的可变参数,可以使用 interface{} 空接口,当然也可以使用结构体

func Printf(format string,args ...interface{}){ ... }  //可接收多种类型的参数
匿名函数与闭包

匿名函数

fplus:=func(x,y int) int { return x+y }  //匿名函数 将匿名函数的地址赋值给fplus
fplus(3,4)  //调用函数
// 另一种调用匿名函数的方式
func(x,y int) int { return x+y }(3,4)   //直接调用

闭包

匿名函数同样被称为闭包
闭包允许调用定义在其他环境下的变量,以使得函数能捕捉一些外部状态,如函数被创建时的状态;闭包继承了函数声明时的作用域
闭包常用于包装函数、更加简洁的错误检查等

递归函数

递归就是在运行过程中调用自己
在使用递归时,必须设置退出条件,否则递归会陷入无限循环
递归在使用时有时会遇到栈溢出问题,大量递归调用导致程序栈内存分配耗尽

内置函数

内置函数不需要导入就能使用

常见内置函数:
close,用于 channal 通信
len,返回某个类型的长度或数量 字符串、数组、切片、map、管道
cap,容量 返回某个类型的最大容量 切片、map
new/make,分配内存
注:new 用于值类型和用户定义的类型,如自定义结构
......new(T) 分配类型T的零值并反回指向类型T的指针,如 v:=new(int)
注:make 用于内置引用类型(切片、map、管道)
......make(T) 返回类型T初始化之后的值,比new做更多的工作
copy/append,复制和连接切片
panic/recover,错误处理
print/println,底层打印函数(建议使用fmt包中的打印函数)
complex/real/imag,创建和操作复数

函数进阶
参数传递机制

参数传递:按值传递 按引用传递
Go默认采用按值传递,即传递的是参数的副本,对该副本的修改,不会影响原参数
按引用传递,传递的是参数的地址(&),可直接修改原参数  复制的是地址

传递指针的优点:
1、能让多个函数操作同一个对象
2、传指针比较轻量级( 8B ),只传递内存地址,可传递体积大的结构体
通常,在函数调用时,切片、字典、接口、通道这样的引用类型默认使用引用传递(即便没有显式地指出指针)
切片、通道、字典这三种类型实现机制类似指针,所以可以直接传递,而不用取地址后传递指针
若函数要改变切片的长度,仍需取地址传递指针
3、传递指针可节省内存,能直接修改变量值,不需要使用return返回

defer 与跟踪

defer 中的表达式必须是函数调用,因此 defer 后不能执行操作语句

defer 语句的用途

进行IO操作时,通过defer关闭文件
打开文件后,可以立即使用 defer 关闭文件,代码更优雅

执行顺序的问题

defer return 返回值之间的关系
反回值是否被命名
匿名函数可以继承变量的值

return 实现逻辑
1、给返回值赋值,若为有名返回值则直接赋值;若为匿名返回值则先声明再赋值
2、调用RET返回指令并传入返回值,RET会检查 defer 是否存在,若存在先逆序插播 defer 语句
3、最后RET携带返回值退出函数

defer 声明时会先计算确定参数的值,推迟执行的仅是函数体,因此 defer 语句位置并非随意,会受到外部影响

defer return 返回值三者顺序:
1、return 最先给返回值赋值

2、defer 执行收尾工作
3、RET指令携带返回值退出函数

跟踪

defer 常用于代码执行跟踪,在进入和离开某个函数时打印相关信息
如下,注意工作函数 trace 和 un

使用 defer 语句还可以记录函数的参数与返回值,如

defer func(){ log.Printf("funcName(%q)=%d,%v",s,n,err) }()

使用 defer 还可以修改return返回的值,如修改 return的错误信息等

错误与恢复

Go语言没有提供异常机制,所以不能抛出异常;Go提供 panic 和 recover 机制
注意:panic 应作为最后的手段来使用,尽可能不使用 panic

声明格式
func panic( interface{} )
func recover() interface{}

1、error
type error interface { Error() string }
error 接口实现错误处理,通常将 error 作为多返回值中的最后一个返回,如
func Foo(param int)(n int, err error) { ... }

使用自写义 error 类型

2、panic
panic是内建函数,用于中断原有控制流程,进入panic流程中
panic可直接调用产生,也可由运行时错误产生
错误处理流程:
执行panic时,函数执行会中断,但函数中 defer 函数会正常执行,然后返回到调用函数的地方,并一层一层向外围扩散,直到 panic 的 goroutine 中所有调用的函数返回,此时程序才退出,错误信息将被报告,包括在调用 panic() 时传入的参数

panic参数是空接口,即接收任意类型的数据

导致关键流程出现不可修复错误时使用panic,其他情况使用error

3、recover
内建函数,将进入 panic 流程的 goroutine 恢复过来;调用 recover 可以捕获到panic的输入值,并恢复正常执行
通常,recover() 只有在 defer 的“函数”内直接调用才会终止错误,否则总是返回 nil
注:在没有发生异常的 goroutine 中明确调用 recover() 会导致该 goroutine 所属进程打印异常信息后直接退出

recover()只会捕获最后一个错误

Go语言不支持泛型,即支持多种类型的函数,解决办法是使用空接口与类型选择,或者使用反射来实现相似的功能
但这些方案会导致代码更为复杂、性能更为低下
在非常注意性能的场合,最好为每一个类型单独创建一个函数

复合数据类型

数组、切片、映射等,因为有切片,实际使用中数组很少使用

数组

数组是具有相同类型的一组已编号且长度固定的数据项序列
数组长度是数组类型的一部分,[20]int 和 [100]int 不是一种类型
数组长度最大为2GB

声明数组
var arr [5]int
arr :=  [5]int{1,3,4,5,8}  //声明并初始化
arr :=  [...]int{1,3,4,5,8}  //容量在初始化时自动计算
arr :=  [5]int{0:10,3:20}  //为特定下标指定具体值,注意下标从0开始

数组元素可通过下标进行访问 arr[0]
数组元素的修改 arr[0]=15

数组的值可以为指针

数组是类型值,可用在赋值操作中(同种类型的数组,即元素类型和长度相同)

var arr1 [5]string
arr2 := [5]string{"a", "b", "c", "d", "e"}
arr1 = arr2
var arr1 [4]*string
arr2 := [4]*string{new(string), new(string), new(string)}  //指针数组的初始化
*arr2[2] = "school"

多维数组

声明二维数组
//第一个是外围数组长度,第二个是内侧数组长度
var arr [4][2]int
arr:=[4][2]int{{1,2},{3,4},{5,6},{7,8}}
arr:=[4][2]int{0:{1,2},2:{5,6}}  //初始化第0和第2个元素
arr:=[4][2]int{0:{0:1}}  //初始化外层和内层的单个元素
arr[0][0]=3  //多维数组的赋值

将数组传递给函数

将数组传递给函数会传递数组的一个副本,开销很大
正确的作法是只传递数组的指针(8字节),如
注意,因为传递的是数组的指针,所以函数中对数组的修改会直接修改原数组,这时可以使用切片

func foo(array *[1e6]int){...}
var arrar [1e6]int  //长度为100万的数组
foo(&array)
切片

切片可以理解成动态数组,可按需自动增长和缩小
增长:通过 append() 函数实现
缩小:对切片再切割

创建切片

创建方法的区别是能否确定切片的容量

1、make 和切片字面量
必须指定切片的长度
slice1 := make([]string,5)  //只指定长度时,长度和容量相等
slice1 := make([]string,5,10)  //长度为5,容量为10
创建出来的切片底层数组长度是创建时指定的容量,但初始化后并不能访问所有数组元素,剩余元素可在后期操作中合并到切片
slice1 := []string{"a","b","c","d"}  //使用字面量创建切片
slice1 := []string{99:"a"}  //使用字面量创建长度和容量为100的切片

2、nil 和空切片
var slice1 []int  //nil切片或空切片
slice1 := make([]int,0)
slice1 := make([]int,0,0)
slice1 := []int{}  //通过字面量创建

使用切片

1、赋值和分割
slice[1]=10   //赋值使用[]运算符
对底层数组容量是k的切片 slice[i:j:k] 来说,长度为 j-i 容量为 k-i
切片只能访问到其长度内的元素

2、切片扩容
使用 append() 函数可以增加切片的长度
append() 被调用时,会返回一个包含修改结果的新切片
append() 只能增加新切片的长度,容量可能会改变,也可能不变,取决于被操作切片的可用容量
若切片的底层数组没有足够容量,append() 会创建一个新的底层数组,将现有值复制到新数组里,再追加新的值
append() 会智能地处理底层数组的容量,容量小于1000时,总是成倍增加,超过1000后,增长因子为1.25

slice := []int{10, 20, 30, 40, 50}
newslice := slice[1:3]
newslice = append(newslice, 60)  //append函数

3、遍历切片
使用 range ,返回两个值,一个是当前迭代的索引位置,第二个值是该位置对应元素的副本

len() 返回切片的长度;cap() 返回切片容量

4、限制容量
对底层数组容量是k的切片 slice[i:j:k] 来说,长度为 j-i  容量为 k-i

试图设置的容量若比可用容量还大,会报错

append() 是变参函数,可传递多个追加值;若使用 ... 运算符,可将一个切片所有元素追加到另一个切片里

多维切片

将切片传递给函数

函数传递切片是以值的方式传递
但切片尺寸很小,成本很低
# 64位架构机器上,切片需要24B的内存:指针8B,长度8B,容量8B
与切片关联的数据包含在底层数组里,不属于切片本身,复制切片不会涉及底层数组

映射

键值存储,映射(map)是无序集合,底层用哈希表实现,是一个hash数组列表

映射的创建

dict := make(map[string]int)
dict := map[string]string{"a":"b","c":"d"}
dict := map[int][]string{}  //值为切片的映射

映射的键的类型可以是内置类型或结构类型,要求是键的值可以使用 == 运算符做比较
注意:切片、函数及包含切片的结构由于具有引用语义,不能作为映射的键
映射的值可以为任意类型,如切片

映射的使用

1、元素赋值
colors := map[string]string{}
colors["red"] = "#DA1337"
注意,声明未初始化的映射 即,nil映射;nil 映射不能用于存储键值对,即nil映射不能赋值

2、查找与遍历

注意,通过键来索引映射时,若键不存在,会返回该值类型的零值

3、元素删除
内置函数 delete() 用于删除元素
当要删除的键不存在时,什么都不发生
delete(colors,"Red")

将映射传递给函数

映射传递给函数是引用传递,函数中对映射的修改会反映到原映射上

切片或映射传递给函数成本很小,不会复制底层的数据结构
切片和映射的底层数据结构是数组

 

转载请注明:轻风博客 » Go语言学习笔记(2)-函数/复合数据类型

喜欢 (0)or分享 (0)