Go 函数
函数声明
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func 函数名(形式参数列表)(返回值列表){
函数体
}
函数的返回值
go是支持多个返回值的,而反观javascript只支持单返回值,js如果面临要返回多个返回值的情况时,只能选择返回一个结构体
go可以对返回值进行命名
func functionName ()(a,b int){
a=2;
b=3;
return
}
我们在第一行命名了两个变量a,b作为返回值,当我们使用命名返回值时,我们在return后可以不写,当然如果写也是可以的
func functionName()(a,b int){
a=2
return a,3
}
这段代码和上一段所表达的含义是相同的
函数体内声明的变量,只能在函数体内进行使用,如果函数运行结束,那么这些变量将会被释放且失效
函数变量
在go中函数是作为一种变量存在的
func fun1 (){
fmt.Printf("hello world")
}
fun2 := fun1
fun2()
hello world
fun2的运行结果是和fun1一样的
匿名函数
匿名函数不需要定义函数名,只有函数声明和函数体
func (形参列表)返回值{
函数体
}
匿名函数也可以即时调用
func (a int){
printf(a)
}(100)
就相当于声明的时候直接调用,传给形参a值为100
匿名函数赋值
因为函数在go中属于一种变量,所以我们声明一个常量代表匿名函数
a:=func (){
}
a()
a就代表这个匿名函数的值
匿名函数作为回调函数
匿名函数可以用作回调函数来进行使用
func visit (arr []int , f func(int)){
for a := range arr{
f(a)
}
}
func main(){
visit([]int{
1,2,3},func(v int){
fmt.Printf(v)
})
}
函数实现接口与结构体
闭包
函数+引用环境 = 闭包
闭包的应用场景主要是,当函数在不同的应用场景被实例化就会成为不同的闭包,闭包是引用了自由变量的函数,闭包一旦形成,即使离开了引用环境,自由变量还是可以自由使用,不影响在闭包中的继续使用
函数是不具有记忆性的,只有闭包才具有,编译期静态为函数,运行期动态称为闭包
package main
import "fmt"
func bibao(i int) func() int {
return func() int {
i++
fmt.Printf("i:%d\n", i)
return i
}
}
func main() {
bibao1 := bibao(1)
for i := 0; i < 10; i++ {
bibao1()
}
}
i:2
i:3
i:4
i:5
i:6
i:7
i:8
i:9
i:10
i:11
闭包会将捕获到闭包中的变量记忆下来,该变量将会跟随闭包的生命周期而一直存在,bibao函数被实例化为bibao1的时候 就已经形成了闭包
如果我们将for中执行的函数变为bibao(1)() ,函数被重复实例化形成多个闭包,我们得到的值将一直是2
可变参数
可变参数就是你想传几个参数就给函数传几个参数,不受限制的传入,函数接收参数时是按照数组切片的方式进行接收
func functionName(形参名 ...int){
for _,value = range 形参名{
value // 参数
}
}
functionName(1,2,3)
这种声明方法是固定类型的传参,…type的形式严格意义上是一种语法糖,且必须要在最后一个参数
任意类型的可变参数
只需要在进行函数声明时,将…type更换为…interface{}
func functionName(args ...interface{
}){
for _,value = range args{
switch value.(type){
// 判断每个元素的类型
case int :
fmt.Println("number");
case string :
fmt.Println("string");
...
defalut:
fmt.Println("unkonw")
}
}
}
Go语言defer (延迟执行语句)
在defer之后的语句会被延时执行,defer后面的语句会被放在该函数的返回之前进行执行,当函数运行到return时,会通知defer开始执行,这时如果有多个defer存在的话,将会按照栈的方式,从最后一个声明的defer后的语句开始执行,倒序完成执行,然后执行return
func fun() string {
fmt.Println("action")
defer fmt.Println("defer1")
defer fmt.Println("defer2")
fmt.Println("end")
return "next action"
}
func main() {
fmt.Println(fun())
}
函数输出结果如下
action
end
defer2
defer1
next action
我们可以看出,defer是运行在函数体之后并且倒序完成,但是又是在函数完成之前进行的,函数完成可以是正常运行结束,return或者是宕机时
使用defer在函数退出时释放资源
在我们学到锁这一章节时,我们会明白defer的优势
var (
// 一个演示用的映射
valueByKey = make(map[string]int)
// 保证使用映射时的并发安全的互斥锁
valueByKeyGuard sync.Mutex
)
// 根据键读取值
func readValue(key string) int {
// 对共享资源加锁
valueByKeyGuard.Lock()
// 取值
v := valueByKey[key]
// 对共享资源解锁
valueByKeyGuard.Unlock()
// 返回值
return v
}
func main(){
i := readValue("one")
fmt.Println(i)
a := readValue("two")
fmt.Println(a)
}
我们简单介绍一下锁,明了一下,这里用到的锁就是防止你重复多线程调用更改
0
0
如果我们不调用解锁unLock的话我们在第二次打印的时候就会得到报错的信息
使用defer来优化上一段代码
func readValue(key string) int {
valueByKeyGuard.Lock()
// defer后面的语句不会马上调用, 而是延迟到函数结束时调用
defer valueByKeyGuard.Unlock()
return valueByKey[key]
}
递归函数
go支持递归的调用,也就是函数内调用函数自身
递归函数除了传入的参数规模不同,其余处理逻辑方面是完全相同的
递归函数一定要有终止条件,否则函数会无限的递归下去,直到出现内存溢出的情况
我们以阶乘举例(阶乘就是给定一个非负数,始终乘以-1的数字,直到乘到0为止,0的阶乘为1)
func Factorial(n uint64)(res uint64){
if n>0{
res = n*Factorial(n-1)
return
}else{
return 1
}
}
func main(){
fmt.Println(Factorial(3))
}
6
也可以实现两个函数之间相互递归的形式,因为go的特性,不在乎函数在哪里声明以及声明顺序
package main
import "fmt"
func main() {
fmt.Println(singular(2))
}
func singular(value uint64) string {
value--
if value == 0 {
return "singular"
} else {
return plural(value)
}
}
func plural(value uint64) string {
value--
if value == 0 {
return "plural"
} else {
return singular(value)
}
}
这是一个判断元素单复数的函数之间相互迭代的一个很好的例子
处理运行时错误
这一步类似于js中的throw Error()但是上层逻辑并不需要为函数的异常而给出太多的资源,在go中,更希望开发者将错误处理作为正常开发中的一些必须实现的环节
我们自定义一个错误
errors是go语言的一个包
package main
import (
"errors"
"fmt"
)
// 定义除数为0的错误
var errDivisionByZero = errors.New("division by zero")
func div(dividend, divisor int) (int, error) {
// 判断除数为0的情况并返回
if divisor == 0 {
return 0, errDivisionByZero
}
// 正常计算,返回空错误
return dividend / divisor, nil
}
func main() {
fmt.Println(div(1, 0))
}
0,division by zero
在解析当中自定义错误信息
在很多时候如果我们直接使用errors.New()是一个很不自由的行为,能够携带的错误信息非常有限,所以我们借助自定义结构体就可以自己定义一个错误接口
package main
import (
"fmt"
)
// 声明一个解析错误
type ParseError struct {
Filename string // 文件名
Line int // 行号
}
// 实现error接口,返回错误描述
func (e *ParseError) Error() string {
return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
// 创建一些解析错误
func newParseError(filename string, line int) error {
return &ParseError{
filename, line}
}
func main() {
var e error
// 创建一个错误实例,包含文件名和行号
e = newParseError("main.go", 1)
// 通过error接口查看错误描述
fmt.Println(e.Error())
// 根据错误接口具体的类型,获取详细错误信息
switch detail := e.(type) {
case *ParseError: // 这是一个解析错误
fmt.Printf("Filename: %s Line: %d\n", detail.Filename, detail.Line)
default: // 其他类型的错误
fmt.Println("other error")
}
}
当解析错误时,会发挥文件名和错误行号
main.go 1
Filename:main.go Line:1
宕机
go中有很多错误会在语言编译的时候就进行呈现,但是很多错误需要在运行时才能够发生,比如数组访问越界,空指针引用等,这时候就会发生宕机。
发生宕机时,程序会中断运行,然后开始运行宕机之前定义的defer语句
panic可以在程序中手动触发宕机,使得损失减小
package main
func main() {
panic("crash")
}
终端返回信息:
panic: crash
goroutine 1 [running]:
main.main()
/Users/a11111/Desktop/code/golang/study/day4/main.go:4 +0x30
exit status 2
我们可以很清晰的看出宕机信息以及具体行数,就可以很准确的去定位到错误的位置,panic方法的意图是为了让go程序直接崩溃
宕机恢复
因为go中没有类似于try/catch的操作,但是有时候我们需要对宕机的情况进行恢复,所以就有一个名为recover的方法对应panic出现
recover只能在defer语句中有效,如果函数正常运行没有触发宕机事件,recover则会返回nil,如果陷入了goroutine的情况下,recover就可以捕获到panic的值并重新开始正常的工作
package main
import (
"fmt"
)
func main() {
savePanic(func() {
fmt.Println("手动触发宕机")
panic("发生了宕机")
})
}
func savePanic(entry func()) {
defer func() {
err := recover()
fmt.Println(err)
fmt.Println("处理宕机")
}()
entry()
}
panic和recover的关系:
有panic没有recover 程序发生宕机
有panic有recover,程序不会宕机,执行defer语句之后,从宕机点退出,函数继续执行
在panic中可以进一步的使用panic 知道程序完全崩溃时
错误是值,所以我们可以直接使用命令返回值来进行设置
计算函数的执行时间
函数的运行时间重要性不用多说
func Since(t Time)Duration
Since()函数是time包中用来获取函数运行时间的
Since的返回值是从t开始到现在的时间,等价于time.Now().Sub(t)
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
for i := 0; i < 1000; i++ {
}
end := time.Since(start)
fmt.Println("运行时间为", end)
}
运行时间为:583ns
上面我们提到了time.Now.Sub(t)也有相同的效果
func main() {
start := time.Now()
for i := 0; i < 1000; i++ {
}
end := time.Now().Sub(start)
fmt.Println("运行时间为", end)
}
我们不难发现,其实得到的结果是相似的,得到的结果不同可能是因为在此时cpu的运行状态不同,属于正常范围之内
test测试函数
为了我们在项目复杂的情况下,尽量去减少bug的考虑,主要有两种方法,一种是代码审核,一种也比较简单就是代码测试
go中使用testing包来完成测试
- 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
- 测试用例的文件名必须以_test.go结尾;
- 需要使用 import 导入 testing 包;
- 测试函数的名称要以Test或Benchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;
- 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
- 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。
testing 包总共有三种测试方法,分别是压力(性能)测试,单元(功能)测试,和覆盖率测试
性能测试
package main
import "testing"
func TestGetArea(t *testing.T) {
area := getArea(10, 20)
if area < 2000 {
t.Error("测试失败")
}
}
a11111@LyiZrideMacBook-Pro day4 % go test -v
=== RUN TestGetArea
main_test.go:8: 测试失败
--- FAIL: TestGetArea (0.00s)
压力测试
func BenchmarkGetArea(t *testing.B) {
for i := 0; i < t.N; i++ {
getArea(100, 120)
}
}
a11111@LyiZrideMacBook-Pro day4 % go test -bench="."
goos: darwin
goarch: arm64
pkg: main.go
BenchmarkGetArea-8 1000000000 0.3193 ns/op
PASS
ok main.go 1.718s
测试了1000000000次,总用时0.3193纳秒
覆盖率测试
覆盖率能够测试程序总共覆盖了多少的业务代码,也就是 main_test.go总共测试了多少的main.go的代码 最好是100%
main_test.go
package main
import "testing"
func TestGetArea(t *testing.T) {
area := getArea(10, 20)
if area < 2000 {
t.Error("测试失败")
}
}
func BenchmarkGetArea(t *testing.B) {
for i := 0; i < t.N; i++ {
getArea(100, 120)
}
}
main.go的代码
package main
func getArea(height int, witdh int) int {
return height * witdh
}
% go test -cover
--- FAIL: TestGetArea (0.00s)
main_test.go:8: 测试失败
FAIL
coverage: 100.0% of statements
exit status 1
FAIL main.go 1.304s
可以看到 覆盖率为100%