Go语言学习笔记(二)

文章目录

  • 六、数组和map
    • 数组
    • 多维数组
    • 切片
      • 直接声明新的切片
      • 使用 make() 函数构造切片
    • map
      • 遍历map
      • 删除
      • 线程安全的map
    • new和make
  • 七、nil
  • 八、条件判断&循环
    • ifelse
    • for
    • for range
    • switch
    • goto
    • break
    • continue
  • 九、函数
    • 函数的参数
    • 匿名函数
    • 闭包
    • 延迟调用
  • 十、异常

六、数组和map

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。

因为数组的长度是固定的,所以在Go语言中很少直接使用数组。

Go语言数组的声明:

var 数组变量名 [元素数量]Type
  • 数组变量名:数组声明及使用时的变量名。
  • 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
  • Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。

给数组赋值:

  1. 初始化的时候赋值

    var arr [3]int = [3]int{1,2,3}
    //如果第三个不赋值,就是默认值0
    var arr [3]int = [3]int{1,2}
    //可以使用简短声明
    arr := [3]int{1,2,3}
    //如果不写数据数量,而使用...,表示数组的长度是根据初始化值的个数来计算
    arr := [...]int{1,2,3}
    
  2. 通过索引下标赋值

    	var arr [3]int
    	arr[0] = 5
    	arr[1] = 6
    	arr[2] = 7
    

一定要注意,数组是定长的,不可更改,在编译阶段就决定了

小技巧: 如果觉的每次写 [3]int 有点麻烦,你可以为 [3]int 定义一个新的类型。

	type arr3 [3]int
	//这样每次用arr3 代替[3]int,注意前面学过 定义一个类型后 arr3就是一个新的类型
	var arr arr3
	arr[0] = 2
	for index,value := range arr{
		fmt.Printf("索引:%d,值:%d \n",index,value)
	}

如果想要只初始化第三个值怎么写?

	//2 给索引为2的赋值 ,所以结果是 0,0,3
	arr := [3]int{2:3}
	for index,value := range arr{
		fmt.Printf("索引:%d,值:%d \n",index,value)
	}

数组比较

如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误:无法比较 [2]int == [3]int

多维数组

Go语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。

声明多维数组的语法如下所示:

//array_name 为数组的名字,array_type 为数组的类型,size1、size2 等等为数组每一维度的长度。
var array_name [size1][size2]...[sizen] array_type

二维数组是最简单的多维数组,二维数组本质上是由多个一维数组组成的。

// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}

只要类型一致,就可以将多维数组互相赋值,如下所示,多维数组的类型包括每一维度的长度以及存储在元素中数据的类型:

// 声明两个二维整型数组 [2]int [2]int
var array1 [2][2]int  
var array2 [2][2]int
// 为array2的每个元素赋值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
// 将 array2 的值复制给 array1
array1 = array2

因为数组中每个元素都是一个值,所以可以独立复制某个维度,如下所示:

// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将数组中指定的整型值复制到新的整型变量里
var value int = array1[1][0]

切片

切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。

与数组不同的是,无法通过切片类型来确定其值的长度。

每个切片值都会将数组作为其底层数据结构。

我们也把这样的数组称为切片的底层数组

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型。可以理解为动态数组

这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内(左闭右开的区间)。

Go语言中切片的内部结构包含地址大小容量,切片一般用于快速地操作一块数据集合。

从连续内存区域生成切片是常见的操作,格式如下:

slice [开始位置 : 结束位置]

语法说明如下:

  • slice:表示目标切片对象;
  • 开始位置:对应目标切片对象的索引;
  • 结束位置:对应目标切片的结束索引。

从数组生成切片,代码如下:

var a  = [3]int{1, 2, 3}
//a[1:2] 生成了一个新的切片
fmt.Println(a, a[1:2])

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置(a[:2])
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾(a[0:])
  • 两者同时缺省时,与切片本身等效(a[:])
  • 两者同时为 0 时,等效于空切片,一般用于切片复位(a[0:0])

注意:超界会报运行时错误,比如数组长度为3,则结束位置最大只能为3

切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。

示例

切片和数组密不可分,如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者,出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片

var highRiseBuilding [30]int
for i := 0; i < 30; i++ {
     highRiseBuilding[i] = i + 1
}
// 区间
fmt.Println(highRiseBuilding[10:15])
// 中间到尾部的所有元素
fmt.Println(highRiseBuilding[20:])
// 开头到中间指定位置的所有元素
fmt.Println(highRiseBuilding[:2])

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合。

切片类型声明格式如下:

//name 表示切片的变量名,Type 表示切片对应的元素类型。
var name []Type
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)

切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。

	var strList []string
	// 追加一个元素
	strList = append(strList,"66666")
	fmt.Println(strList)

使用 make() 函数构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

make( []Type, size, cap)

Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题

a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
//容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2
//但如果我们给a 追加一个 a的长度就会变为3
fmt.Println(len(a), len(b))

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

map

map 是一种无序的键值对的集合。

map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,map 是无序的,我们无法决定它的返回顺序,这是因为 map 是使用 hash 表来实现的。

map 是引用类型,可以使用如下方式声明:

//[keytype] 和 valuetype 之间允许有空格。
var mapname map[keytype]valuetype

其中:

  • mapname 为 map 的变量名。
  • keytype 为键类型。
  • valuetype 是键对应的值类型。

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 键值对的数目。

package main
import "fmt"
func main() {
    var mapLit map[string]int
    var mapAssigned map[string]int
    mapLit = map[string]int{"one": 1, "two": 2}
    mapAssigned = mapLit
    //mapAssigned 是 mapList 的引用,对 mapAssigned 的修改也会影响到 mapList 的值。
    mapAssigned["two"] = 3
    fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])
    fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"])
    fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"])
}

map的另外一种创建方式:

make(map[keytype]valuetype)

切记不要使用new创建map,否则会得到一个空引用的指针

map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:

make(map[keytype]valuetype, cap)

例如:

map2 := make(map[string]int, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?

答案是:使用切片

例如,当我们要处理 unix 机器上的所有进程,以父进程(pid 为整形)作为 key,所有的子进程(以所有子进程的 pid 组成的切片)作为 value。

通过将 value 定义为 []int 类型或者其他类型的切片,就可以优雅的解决这个问题,示例代码如下所示:

mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

遍历map

map 的遍历过程使用 for range 循环完成,代码如下:

scene := make(map[string]int)
scene["cat"] = 66
scene["dog"] = 4
scene["pig"] = 960
for k, v := range scene {
    fmt.Println(k, v)
}

注意:map是无序的,不要期望 map 在遍历时返回某种期望顺序的结果

删除

使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:

delete(map,)

map 为要删除的 map 实例,键为要删除的 map 中键值对的键。

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

注意map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

线程安全的map

需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。

sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

new和make

make 关键字的主要作用是创建 slice、map 和 Channel 等内置的数据结构,而 new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针。

  1. make 分配空间后,会进行初始化,new分配的空间被清零
  2. new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  3. new 可以分配任意类型的数据;

七、nil

在Go语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是 nil。

nil和其他语言的null是不同的。

nil 标识符是不能比较的

package main
import (
    "fmt"
)
func main() {
    //invalid operation: nil == nil (operator == not defined on nil)
    fmt.Println(nil==nil)
}

nil 不是关键字或保留字

nil 并不是Go语言的关键字或者保留字,也就是说我们可以定义一个名称为 nil 的变量,比如下面这样:

//但不提倡这样做
var nil = errors.New("my god")

nil 没有默认类型

package main
import (
    "fmt"
)
func main() {
    //error :use of untyped nil
    fmt.Printf("%T", nil)
    print(nil)
}

不同类型 nil 的指针是一样的

package main
import (
    "fmt"
)
func main() {
    var arr []int
    var num *int
    fmt.Printf("%p\n", arr)
    fmt.Printf("%p", num)
}

nil 是 map、slice、pointer、channel、func、interface 的零值

package main
import (
    "fmt"
)
func main() {
    var m map[int]string
    var ptr *int
    var c chan int
    var sl []int
    var f func()
    var i interface{}
    fmt.Printf("%#v\n", m)
    fmt.Printf("%#v\n", ptr)
    fmt.Printf("%#v\n", c)
    fmt.Printf("%#v\n", sl)
    fmt.Printf("%#v\n", f)
    fmt.Printf("%#v\n", i)
}

零值是Go语言中变量在声明之后但是未初始化被赋予的该类型的一个默认值。

不同类型的 nil 值占用的内存大小可能是不一样的

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var p *struct{}
    fmt.Println( unsafe.Sizeof( p ) ) // 8
    var s []int
    fmt.Println( unsafe.Sizeof( s ) ) // 24
    var m map[int]bool
    fmt.Println( unsafe.Sizeof( m ) ) // 8
    var c chan string
    fmt.Println( unsafe.Sizeof( c ) ) // 8
    var f func()
    fmt.Println( unsafe.Sizeof( f ) ) // 8
    var i interface{}
    fmt.Println( unsafe.Sizeof( i ) ) // 16
}

具体的大小取决于编译器和架构

八、条件判断&循环

ifelse

在Go语言中,关键字if是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。

if condition {
    // 条件为真执行
}

condition 称之为条件表达式或者布尔表达式,执行结果需返回true或false。{ 必须在条件表达式的尾部

如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。

for

go语言中的循环语句只支持 for 关键字,这个其他语言是不同的。

sum := 0
//i := 0; 赋初值,i<10 循环条件 如果为真就继续执行 ;i++ 后置执行 执行后继续循环
for i := 0; i < 10; i++ {
    sum += i
}

第二种写法:

sum := 0
for {
    sum++
    if sum > 100 {
        //break是跳出循环
        break
    }
}

上述的代码,如果没有break跳出循环,那么其将无限循环

结束循环的方式:

  1. return

    step := 2
    for step > 0 {
        step--
        fmt.Println(step)
        //执行一次就结束了
        return
    }
    //不会执行
    fmt.Println("结束之后的语句....")
    
  2. break

    step := 2
    for step > 0 {
        step--
        fmt.Println(step)
        //跳出循环,还会继续执行循环外的语句
        break
    }
    //会执行
    fmt.Println("结束之后的语句....")
    
  3. painc

    step := 2
    for step > 0 {
    		step--
    		fmt.Println(step)
    		//报错了,直接结束
    		panic("出错了")
    	}
    	//不会执行
    	fmt.Println("结束之后的语句....")		
    
  4. goto

    package main
    import "fmt"
    func main() {
        for x := 0; x < 10; x++ {
            for y := 0; y < 10; y++ {
                if y == 2 {
                    // 跳转到标签
                    goto breakHere
                }
            }
        }
        // 手动返回, 避免执行进入标签
        return
        // 标签
    breakHere:
        fmt.Println("done")
    }
    

for range

for range 结构是Go语言特有的一种的迭代结构,for range 可以遍历数组、切片、字符串、map 及管道(channel)

for key, val := range coll {
    ...
}

val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值

遍历map:

m := map[string]int{
    "hello": 100,
    "world": 200,
}
for key, value := range m {
    fmt.Println(key, value)
}

字符串也可以使用for range

每个 rune 字符和索引在 for range 循环中是一一对应的,它能够自动根据 UTF-8 规则识别 Unicode 编码的字符。

通过 for range 遍历的返回值有一定的规律:

  • 数组、切片、字符串返回索引和值。
  • map 返回键和值。
  • channel只返回管道内的值。

switch

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。

switch 语句的语法如下:

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值

类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。

Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 那么如何做到执行完一个case之后,进入下一个case而不是跳出swtich呢?

答案是:fallthrough

var s = "hello"
switch {
case s == "hello":
    fmt.Println("hello")
    fallthrough
case s != "world":
    fmt.Println("world")
}

注意事项:加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行

goto

goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。

使用 goto 语句后,无须额外的变量就可以快速退出所有的循环

使用 goto 集中处理错误

多处错误处理 存在代码重复 例如:

package main

import (
	"errors"
	"fmt"
	"os"
)


func main() {
	err := firstCheckError()
	if err != nil {
		fmt.Println(err)
		exitProcess()
	}
	err = secondCheckError()
	if err != nil {
		fmt.Println(err)
		exitProcess()
	}
	fmt.Println("done")
}

func secondCheckError() interface{} {
	return errors.New("错误2")
}

func exitProcess() {
	//退出
	os.Exit(1)
}

func firstCheckError() interface{} {
	return errors.New("错误1")
}

使用goto:

package main

import (
	"errors"
	"fmt"
	"os"
)


func main() {
	err := firstCheckError()
	if err != nil {
		fmt.Println(err)
		goto onExit
	}
	err = secondCheckError()
	if err != nil {
		fmt.Println(err)
		goto onExit
	}
	fmt.Println("done")
	return
	onExit:
		exitProcess()
}

func secondCheckError() interface{} {
	return errors.New("错误2")
}

func exitProcess() {
	fmt.Println("exit")
	//退出
	os.Exit(1)
}

func firstCheckError() interface{} {
	return errors.New("错误1")
}

break

break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 forswitchselect 的代码块上。

package main
import "fmt"
func main() {
OuterLoop:
    for i := 0; i < 2; i++ {
        for j := 0; j < 5; j++ {
            switch j {
            case 2:
                fmt.Println(i, j)
                break OuterLoop
            case 3:
                fmt.Println(i, j)
                break OuterLoop
            }
        }
    }
}

continue

continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环

package main
import "fmt"
func main() {
OuterLoop:
    for i := 0; i < 2; i++ {
        for j := 0; j < 5; j++ {
            switch j {
            case 2:
                fmt.Println(i, j)
                continue OuterLoop
            }
        }
    }
}

九、函数

函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,其可以提高应用的模块性和代码的重复利用率。

Go 语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。

Go 语言的函数属于“一等公民”(first-class),也就是说:

  • 函数本身可以作为值进行传递。
  • 支持匿名函数和闭包(closure)。
  • 函数可以满足接口。

函数定义:

func function_name( [parameter list] ) [return_types] {
   函数体
}
  • func:函数由 func 开始声明
  • function_name:函数名称,函数名和参数列表一起构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • 函数体:函数定义的代码集合。

Go语言是编译型语言,所以函数编写的顺序是无关紧要的,鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。

函数做为一等公民,可以做为参数传递。

在将函数做为参数的时候,我们可以使用类型定义,将函数定义为类型,这样便于阅读。有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

函数返回值可以有多个,同时Go支持对返回值命名

函数的参数

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。

形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

  1. 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

    func swap(x, y int) int {
           ... ...
      }
    
  2. 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

    package main
    
    import (
    	"fmt"
    )
    
    /* 定义相互交换值的函数 */
    func swap(x, y *int) {
    	*x,*y = *y,*x
    }
    
    func main() {
    	var a, b int = 1, 2
    	/*
    	   调用 swap() 函数
    	   &a 指向 a 指针,a 变量的地址
    	   &b 指向 b 指针,b 变量的地址
    	*/
    	swap(&a, &b)
    
    	fmt.Println(a, b)
    }
    

在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

注意2:map、slice、chan、指针、interface默认以引用的方式传递。

不定参数传值

不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)

Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。

在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片。

格式:

  func myfunc(args ...int) {    //0个或多个参数
  }

  func add(a int, args…int) int {    //1个或多个参数
  }

  func add(a int, b int, args…int) int {    //2个或多个参数
  }

注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.

匿名函数

匿名函数是指不需要定义函数名的一种函数实现方式。

在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。

匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必声明。

匿名函数的定义格式如下:

func(参数列表)(返回参数列表){
    函数体
}

示例:

package main

import (
    "fmt"
    "math"
)

func main() {
    //这里将一个函数当做一个变量一样的操作。
    getSqrt := func(a float64) float64 {
        return math.Sqrt(a)
    }
    fmt.Println(getSqrt(4))
}

在定义时调用匿名函数

匿名函数可以在声明后调用,例如:

func(data int) {
    fmt.Println("hello", data)
}(100) //(100),表示对匿名函数进行调用,传递参数为 100。

匿名函数用作回调函数

匿名函数作为回调函数的设计在Go语言也比较常见

闭包

所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

闭包=函数+引用环境

示例:

package main

import "fmt"

// 返回一个闭包函数
func counter() func() int {
    count := 0  // 外部变量,闭包将引用它
    return func() int {
        count++  // 闭包函数访问并修改外部变量
        return count
    }
}

func main() {
    // 获取一个计数器
    c1 := counter()

    fmt.Println(c1())  // 输出 1
    fmt.Println(c1())  // 输出 2
    fmt.Println(c1())  // 输出 3

    // 获取另一个独立的计数器
    c2 := counter()

    fmt.Println(c2())  // 输出 1,c2 是独立的
    fmt.Println(c2())  // 输出 2
}

延迟调用

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理

defer特性:

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执行。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer的用途:

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放

go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。

看下面的示例:

package main

import (
	"log"
	"time"
)

func main() {
	start := time.Now()
	log.Printf("开始时间为:%v", start)
  defer log.Printf("时间差:%v", time.Since(start))  // Now()此时已经copy进去了
    //不受这3秒睡眠的影响
	time.Sleep(3 * time.Second)

	log.Printf("函数结束")
}
  • Go 语言中所有的函数调用都是传值的
  • 调用 defer 关键字会立刻拷贝函数中引用的外部参数 ,包括start 和time.Since中的Now
  • defer的函数在压栈的时候也会保存参数的值,并非在执行时取值

如何解决上述问题:使用defer fun()

因为拷贝的是函数指针,函数属于引用传递

在来看一个问题:

package main

import "fmt"

func main() {
	var whatever = [5]int{1,2,3,4,5}
	for i,_ := range whatever {
        //函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
		defer func() { fmt.Println(i) }()
	}
}

怎么解决:

package main

import "fmt"

func main() {
	var whatever = [5]int{1,2,3,4,5}
	for i,_ := range whatever {
		i := i
		defer func() { fmt.Println(i) }()
	}
}

十、异常

Go语言中使用 panic 抛出错误,recover 捕获错误。

异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic:

  1. 内置函数
  2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
  3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
  4. 直到goroutine整个退出,并报告错误

recover:

  1. 内置函数
  2. 用来捕获panic,从而影响应用的行为

golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 defer 语句,再报告异常信息,最后退出 goroutine。如果在 defer 中使用了 recover() 函数,则会捕获错误信息,使该错误信息终止报告。

注意:

  1. 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  3. 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main

func main() {
    test()
}

func test() {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string)) // 将 interface{} 转型为具体类型。
        }
    }()

    panic("panic error!")
}

由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。

 func panic(v interface{})
 func recover() interface{}

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获:

package main

import "fmt"

func test() {
    defer func() {
        // defer panic 会打印
        fmt.Println(recover())
    }()

    defer func() {
        panic("defer panic")
    }()

    panic("test panic")
}

func main() {
    test()
}

如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码被执行 :

package main

import "fmt"

func test(x, y int) {
    var z int

    func() {
        defer func() {
            if recover() != nil {
                z = 0
            }
        }()
        panic("test panic")
        z = x / y
        return
    }()

    fmt.Printf("x / y = %d\n", z)
}

func main() {
    test(2, 1)
}

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态:

type error interface {
    Error() string
}

标准库 errors.Newfmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

package main

import (
    "errors"
    "fmt"
)

var ErrDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
    if y == 0 {
        return 0, ErrDivByZero
    }
    return x / y, nil
}

func main() {
    defer func() {
        fmt.Println(recover())
    }()
    switch z, err := div(10, 0); err {
    case nil:
        println(z)
    case ErrDivByZero:
        panic(err)
    }
}

Go实现类似 try catch 的异常处理:

package main

import "fmt"

func Try(fun func(), handler func(interface{})) {
    defer func() {
        if err := recover(); err != nil {
            handler(err)
        }
    }()
    fun()
}

func main() {
    Try(func() {
        panic("test panic")
    }, func(err interface{}) {
        fmt.Println(err)
    })
}

如何区别使用 panic 和 error 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

你可能感兴趣的:(Go语言学习之路,golang,学习,笔记)