Golang_03_函数


目录:

函数

为完成某一功能的程序指令(语句)的集合,称为函数

调用机制

  1. 在调用一个函数时,会为该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他的栈空间区分开来;
  2. 每个函数对应的栈中,数据空间是独立的,不会混淆;
  3. 当一个函数执行完毕后,程序会销毁这个函数对应的栈空间;

return语句

  1. 如果返回多个值,在接收时,可以用_符号表示占位忽略;
func [函数名] ([形参列表]) ([返回值列表]) {  // 基本语法    
    [指定语句]    
    return [返回值列表]  // 当函数有return语句时,将结果返回给调用者 
}

递归调用

函数体内调用了本身,称为递归调用;

当一件事需要不断地重复某个固定模式,并且下次模式的执行还依赖这次的执行结果时,就用递归来实现

func test(n int)  {    
    if n > 2 {        
        n--  // 递归必须向退出递归条件逼近        
        test(n)    
    }    
    fmt.Println("n=",n) 
} 
func test2(n int)  {    
    if n > 2 {        
        n--        
        test2(n)    
    } else {        
        fmt.Println("n=",n)    
    } 
} 
func main() {    
    test(4)  // n = 2, n = 2, n = 3    
    test2(4)  // n = 2  
}

总结

  1. 执行一个函数时,就会创建一个新的受保护的独立空间,也可以称为函数栈
  2. 函数的局部变量是独立的,不会相互影响;
  3. 当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁。同时当函数执行完毕或返回时,该函数本身也会被销毁;

可变参数

参数数量可变的函数称为可变函数。在声明可变函数的参数时,需要在参数列表的最后一个参数类型之前添加'...',表示该函数会接受任意数量的该类型的参数

  1. 调用者会隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用的函数。如果原始参数已经是切片类型,需要在最后一个参数后加上'...'即可
  2. args是切片,通过 args[index] 可以访问到各个值
func sum(args... int) sum int {}  // 支持0到多个参数 
func sum(n1 int, args... int) sum int {}  // 支持1到多个参数 
func Sum(n1 int,args... int)(result int){    
    result = n1    
    for index ,value := range args{        
        args[index] = value        
        result += args[index]    
    }    
    return 
} 
type MyFuncType func(int, int) int 
func main() {    
    fmt.Println(Sum(1,6,6,2,3,4,1,541,21))  //585    
    value := []int{1,23,62,62,1,35}    
    fmt.Println(Sum(value...))  // 185 
}

⭐函数注意事项和细节

  1. 形参与返回值列表可以是多个;

  2. 形参列表和返回值列表的数据类型可以是值类型和引用类型;

  3. 基本数据类型和数据默认为值传递,即值拷贝。所以在函数内修改时,不会影响到原来的值;

  4. 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&。函数内以指针的方式操作变量;

  5. Go函数不支持函数重载;

  6. 在Go中,函数也是一种数据类型。可以赋给一个变量,则该变量就是一个函数类型的变量,可以通过调用该变量来调用函数;

  7. 函数可以作为形参被调用;

  8. Go支持自定义数据类型
func GetSum(n1 int, n2 int) int {    
    return n1 + n2 
} 
func MyFunc(funvar MyFuncType, num1 int, num2 int ) (int) {    
    return funvar(num1, num2) 
} 
type MyFuncType func(int, int) int  // 根据GetSum的数据类型直接定义一个新的数据类型供函数调用 
func main() {    
    a := GetSum    
    fmt.Printf("a的数据类型=%T \nGetSum的数据类型=%T \n",a,GetSum)  // GetSum的数据类型=func(int, int) int    
    fmt.Println(MyFunc(GetSum, 50, 60)) 
}
  1. 支持对函数返回值命名;
func GetSumAndSub(n1 int, n2 int) (sum int, sub int){  //  返回sum和sum两个int类型的值    
    sum = n1 + n2    
    sub = n1 - n2    
    return } 
func main() {    
    b,c := GetSumAndSub(3, 2)    
    fmt.Printf("b=%v,c=%v",b,c) 
}

init函数

每一个源文件都可以包含一个init函数。该函数会在main函数执行前,被Go运行框架调用。

如果一个文件同时包含全局变量定义,init函数和main函数,则执行流程是导入的包>全局变量定义>init函数>main函数

匿名函数

如果某个函数只是希望使用一次,可以考虑匿名函数。当然,匿名函数也可以实现多次调用,就是将匿名函数赋给一个变量,通过该变量来实现多次调用匿名函数。

匿名函数有以下两种使用方式且代码中还有'全局匿名函数':

  1. 在定义匿名函数时就直接调用
  2. 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
var (    
    Fun1 = func (n1 int, n2 int) int {  // 定义fun1为全局匿名函数        
    return n1 * n2    
    } 
) 
func main()  {    
    res1 := func (n1 int, n2 int) (int) {  // 方式一,在定义匿名函数时就直接调用        
        return n1 + n2    
    }(10,20)  // 参数    
    fmt.Println(res1)  // 输出30        
    a := func (n1 int, n2 int) (int) {  // 方式二,将函数赋给一个变量,通过变量来调用
        return n1 + n2    
    }    
    res2 := a(20,30)    
    fmt.Println(res2)        
    res3 := Fun1(2,4)  // 方式三,调用全局匿名函数    
    fmt.Println(res3) 
}

细节注意:在for循环中生成的所有函数值都共享相同的循环变量,即函数值中记录的是循环变量的内存地址,所以当循环结束后,接收函数变量的值等于最后一次迭代的值,可以通过引入另一个变量来存储值。

使用godefer语句的时候可能经常遇到该问题,是因为它们都会等待循环结束后,再执行函数值。

闭包

闭包就是一个函数和与其相关的引用环境组合的一个 (实体),具体使用看下面案例(从该案例可以看到变量的声明周期不由它的作用域决定,即AddUpper()返回后,变量n仍然隐式的存在于f中)。

AddUpper()中定义的匿名内部函数可以访问和更新AddUpper()中的局部变量,这意味着匿名函数和AddUpper()中,存在变量引用。这就是函数值属于引用类型和函数值不可比较的原因。

func AddUpper() func (int) int {  // AddUpper是一个函数,返回的数据类型时fun(int) int    
    var n int = 10    
    return func (x int) int {        
        n += x        
        return n    
    } 
} 
func main()  {    
    f := AddUpper()    
    fmt.Println(f(3))  // 13    
    fmt.Println(f(1))  // 14    
    fmt.Println(f(2))  // 16 
}

返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包。闭包可以保留上次引用的某个值。

  1. 可以理解为:闭包是类,函数是操作,n是字段。函数和它使用到n构成闭包;
  2. 当我们反复的调用f函数时,因为n时初始化一次,因此每调用一次就进行累计;
  3. 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包;

最佳实践

func makeSuffix(suffix string) func (string) string {    
    return func (name string) string{        
        if !strings.HasSuffix(name, suffix) {            
            return name + suffix        
        }        
        return name    
    } 
} 
func main()  {    
    f := makeSuffix(".jpg")    
    fmt.Println(f("winter"))  // winter.jpg    
    fmt.Println(f("bird.jpg"))  // bird.jpg 
}

常用的系统函数

字符串中常用的系统函数

  1. strconv // 字符串类型转换
  2. strings // 字符串处理
func main() {    
    // 统计字符串长度,按字节    
    var str string = "daihaorui豪锐"    
    fmt.Println("str=",len(str))     

    // 字符串遍历,同时处理有中文的问题    
    str1 := []rune(str)    
    fmt.Println("str1=",len(str1))     

    // 字符串转整数    
    var str2 string = "123"    
    n, err := strconv.Atoi(str2)    
    fmt.Printf("str2=%v,str2err=%v \n",n,err)     

    // 整数转字符串    
    var num1 int = 22    
    n1 := strconv.Itoa(num1)    
    fmt.Printf("num1=%v,num1Type=%T \n",n1,n1)     

    // 字符串转[]byte: var bytes = []byte("hello go")    
    str3 := "haorui"    
    var bytes = []byte(str3)    
    fmt.Printf("bytes=%v \n",bytes)     

    // []byte 转字符串:str = string([]byte{97,98,99})    
    str = string([]byte{97,98,99})    
    fmt.Printf("str=%v \n",str)     

    // 10进制转2,8,16进制:str = strconv.FormatInt(123,2) // 2->8,16    
    str = strconv.FormatInt(123,2) // 2->8,16    
    fmt.Printf("str=%v \n",str)     

    // 查找子串是否在指定的字符串中:strings.Contains("seafood","foo")  //true
    fmt.Println(strings.Contains("seafood","foo"))     

    // 统计一个字符串有几个指定的字串:strings.Count("ceheese","e")  // 4    
    fmt.Println(strings.Count("ceheese","e"))     

    // 不区分大小写的字符串比较(==是区分字母大小写的)fmt.Println(strings.EquaIfold("abc","Abc"))  // true    
    fmt.Println("abc"=="Abc")   
    fmt.Println(strings.EqualFold("abc","Abc"))     

    // 返回子串在字符串中第一次出现的index值,如果没有则返回-1:strings.Index()
    fmt.Println(strings.Index("NLT_abc","abc"))     

    // 对字符串进行替换:strings.Replace()     

    // 对字符串指定分隔符进行分隔:strings.Split()     

    // 对字符串进行字母大小写转换:strings.ToLower()     

    // 将字符串的指定字符或空格去掉:strings.Trim() & strings.TrimSpace() & strings.TrimRight()     

    // 判断字符串是否以指定字符串开头 & 结尾:strings.HasPrefix() & strings.HasSuffix() 
}

时间和日期函数(time)

内建函数(buildin)

  1. len() //求长度
  2. new() // 用来分配内存,主要用来分配值类型,如int、float32、struct,返回的是指针
  3. make() // 用来分配内存,主要用来分配引用类型,如channel、map、slice
num2 := new(int)  // new(int)会自动分配地址 
var num2 *int  // *int需要手动指定地址

defer函数

在函数中,开发经常需要创建资源(数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go提供了defer(延时机制),释放资源的defer应该直接跟在请求资源的语句后

细节说明:

  1. 遵循先入后出的方式,直到defer执行完毕后才会执行main区中的语句。(在执行到defer时,暂时不执行,会到defer的语句压入到独立的defer栈中,当函数执行完毕后,再从defer栈,按照先入后出的方式出栈)
  2. 在defer将语句放入到栈时,也会将相关的值拷贝同时入栈;
func Sum(n1 *int,n2 *int) int {    
    defer fmt.Println("ok1 n1=",*n1)    
    defer fmt.Println("ok1 &n1=",n1)    
    fmt.Println("ok1 &n1=",n1)    
    defer fmt.Println("ok2 n2=",*n2)    
    *n1++    
    *n2++    
    res := *n1 + *n2    
    fmt.Println("ok3 res=",res)    
    fmt.Println("ok3 &n1=",n1)    
    return res 
} 
func main() {    
    n1 := 10    
    n2 := 20    
    fmt.Println("&n1=",&n1)     
    res := Sum(&n1,&n2)    
    fmt.Println("res=",res)    
    fmt.Println("&n1=",&n1) 
} 
// 输出 
&n1= 0xc000012088 
ok1 &n1= 0xc000012088
ok3 res= 32 
ok3 &n1= 0xc000012088
ok2 n2= 20 
ok1 &n1= 0xc000012088
ok1 n1= 10 
res= 32 
&n1= 0xc000012088gg
  1. defer机制也常用于记录何时进入和退出函数

bigSlowOperation()被调用时,trace()会返回一个函数值,该函数值会在bigSlowOperation()退出时被调用

func bigSlowOperation() {    
    defer trace("bigSlowOperation")() // don't forget the extra parentheses    
    // ...lots of work…    
    time.Sleep(10 * time.Second) // simulate slow operation by sleeping
} 
func trace(msg string) func() {    
    start := time.Now()    
    log.Printf("enter %s", msg)    
    return func() {         
        log.Printf("exit %s (%s)", msg,time.Since(start))     
    } 
} 
func main() {    
    bigSlowOperation() 
} 
$ go run main.go  
2022/08/13 14:37:22 enter bigSlowOperation 
2022/08/13 14:37:24 exit bigSlowOperation (2.000747904s)

Panic异常

当发生panic异常时,程序会执行以下操作:

  1. 中断运行,立即执行在该goroutine中被延迟的函数(defer机制)
  2. 程序崩溃并输出日志信息,日志信息包括panic value(通常是某种错误信息)和函数调用的堆栈跟踪信息

可以通过调用内置panic函数来引发panic异常,panic函数接受任何值作为参数。对于大部分漏洞,应该使用Go提供的错误机制(而不是panic),来尽量避免程序的崩溃。

我们应该假设函数的输入一直合法:当调用这输入了不应该出现的输入时,触发panic异常。

Recover捕获异常

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。未发生panic时调用revover,recover会返回nil

实现:使用panic和recover编写一个不包含return语句但能返回一个非零值的函数

func test() (err error) {    
    defer func() {        
        if p := recover(); p != nil {            
            err = fmt.Errorf("报错=%v", p)        
        }    
    }()    
    panic("输入的报错") 
} 
func main() {
    fmt.Println(test()) 
} 
$ go run main.go  
报错=输入的报错

错误处理(errors)

Go中大部分函数代码结构几乎相同,首先是一系列的初始检查,防止错误发生,之后是函数的实际逻辑。其错误处理方式为,抛出一个panic异常,然后在defer中通过recover捕获这个异常,然后正常处理,用到:

  1. defer
  2. panic
  3. recover
func test() {    
    defer func () {        
        err := recover()        
        if err != nil {            
            fmt.Println("res=",err)        
        }    
    }()    
    num1 := 10    
    num2 := 0    
    res := num1 / num2     
    fmt.Println(res) 
} 
func main() {   
    test()    
    fmt.Println("该行代码正常执行") 
}

自定义错误

Go支持自定义错误,使用errors.New()panic内置函数。

  1. errors.New("错误说明"),会返回一个error类型的值,表示一个错误。
  2. panic内置函数,接收一个interface{}类型的值(也就是任何值)作为参数。可以接收error类型的变量,输出错误信息,并退出程序
func test02 () {    
    err := ReadConf("c2onfig.ini")    
    if err != nil {        
        panic(err)    
    }    
    fmt.Println("test02()继续执行") 
} 
func ReadConf (name string) (err error) {    
    if name == "config.ini" {        
        return nil    
    } else {        
        return errors.New("读取文件错误")    
    }
} 
func main() {    
    test02()  // 执行到该函数的时候报'panic: 读取文件错误',后续代码不执行    
    fmt.Println("该行代码正常执行") 
}

基本概念

Go的每一个文件都属于一个包,也就是Go是以包的形式来管理文件和项目目录结构的

包的作用:

  1. 区分相同名字的函数、变量等标识符;
  2. 当程序文件很多时,可以管理项目;
  3. 控制函数、变量等的访问范围,即作用域;

基本操作

package [包名]  // 归属于指定包 
import "[包的路径]"

注意事项和使用细节

  1. 文件包名通常和文件所在的文件夹名一致,一般为小写字母;
  2. 在import包时,路径从$GOPATH下的src下开始。不用带src,编译器会自动从src下开始引入;
  3. 能被访问到的函数名的首字母大写
  4. 访问其他包的函数、变量时,语法是[包名].[函数名]
  5. 可以对导入的包设置别名,在import的前面,与_相对应;
  6. 如果要编译成一个可执行程序,需要将该包声明为main,即package.main。若是只是编写库,则可以自定义包名;
$ cd D:\MyNutcloud\wlhiot_manage\goproject  // $GOPATH  
$ tree 
D:.
  └─src    
    └─go_code  // 源码管理        
    ├─array  // 项目名称        
      └─main  // 包名 
go build -o [可执行文件名称].exe go_code/array/main  // 格式,以GOPATH路径 
go build -o bin/my.exe .\src\go_code\array\main  // 实际打包

案例

$ ls D:\MyNutcloud\wlhiot_manage\goproject\src\go_code\function02\funcinit 
d----l         2022/7/13     20:32                utils 
-a---l         2022/7/13     21:09             25 go.mod 
-a---l         2022/7/13     21:18            291 main.go 

$ ls D:\MyNutcloud\wlhiot_manage\goproject\src\go_code\function02\funcinit\utils\ 
-a---l         2022/7/13     21:16            151 utils.go 

$ cat .\main.go 
package main 
import (        
    "fmt"        
    "funcinit/utils" 
) 
var age = test() 
func test() int {        
    fmt.Println("test()")        
    return 90 
} 
func init(){        
    fmt.Println("init()...") 
} 
func main() {        
    fmt.Println("main()...age",age)        
    fmt.Println("Age=",utils.Age,"Name=",utils.Name) 
} 

$ cat .\utils\utils.go 
package utils 
import ( 
    "fmt"
) 
var Age int 
var Name string 
func init()  {    
    fmt.Println("utils包的init()")    
    Age = 100    
    Name = "tom" 
} 

$ go run .\main.go 
utils包的init()
test() 
init()...
main()...age 90 
Age= 100 Name= tom