js工作者的go学习笔记-go函数

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(123

这种声明方法是固定类型的传参,…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包来完成测试

  1. 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
  2. 测试用例的文件名必须以_test.go结尾;
  3. 需要使用 import 导入 testing 包;
  4. 测试函数的名称要以Test或Benchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用例文件中可以包含多个测试函数;
  5. 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
  6. 测试用例文件使用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%

猜你喜欢

转载自blog.csdn.net/weixin_44846765/article/details/125059621