七叶笔记 » golang编程 » Golang之函数使用

Golang之函数使用

写在前面:

0x01 — 函数

函数是Golang中使用最频繁的结构之一,在Golang中,函数需要注意的特点:

1. 可以返回多个结果

2. 参数和返回值类型需要明确

3. 如果定义了返回值,可以不进行return默认会返回该值

4. 指针类型参数

5. 函数作为参数

看下示例:

 package main

import "testing"

// 加法函数
func funcAdd(a,b int) int { // 此处等效(a int, b int)
   return a + b
}

func TestFunc(t *testing.T) {
   c, d := 1, 2
   var e int = funcAdd(c, d) // 调用函数
   t.Log("e的值:", e)
}  

输出:

 === RUN   TestFunc
    func_test.go:13: e的值: 3
--- PASS: TestFunc (0.00s)
PASS  

以上是最简单的标准函数的调用,函数参数如果类型相同可以批量定义类型,继续看下去:

 package main

import (
   "sort"
   "testing"
)

// 排序函数
func SortFunc(l []int) []int {
   if len(l) < 2 {
      return l
   } else{
      sort.Ints(l) // 对l进行排序
      return l
   }
}
// 排序函数
func SortFuncPtr(l *[]int) *[]int {
   if len(*l) < 2 {
      return l
   } else{
      sort.Ints(*l) // 对l进行排序
      return l
   }
}

func TestFunc1(t *testing.T) {
   // 定义两个slice
   var a = []int{2,1,3}
   var b = []int{5,4,6}
   // 调用传输变量函数
   var c []int = SortFunc(a)
   // 调用传输指针的函数
   var d *[]int = SortFuncPtr(&b)
   //通过打印c,d的值发现都可以正常排序
   t.Log("c的值:", c)
   t.Log("d的值:", *d) // 因为d是指针,需要取得d的值通过取值符*
   // 我们打印下各自的指针数据,就可以看出区别了
   t.Logf("a的指针:%p", &a)
   t.Logf("b的指针:%p", &b)
   t.Logf("c的指针:%p", &c)
   t.Logf("d的指针:%p", d)
}  

输出:

 === RUN   TestFunc1
    func_test.go:36: c的值: [1 2 3]
    func_test.go:37: d的值: [4 5 6]
    func_test.go:39: a的指针:0xc00000c048
    func_test.go:40: b的指针:0xc00000c060
    func_test.go:41: c的指针:0xc00000c078
    func_test.go:42: d的指针:0xc00000c060
--- PASS: TestFunc1 (0.00s)
PASS  

以上示例主要是介绍指针作为参数的区别,可以看到b的指针和d是样的,也就是在使用指针作为参数传递时,我们可以在不增加内存空间的基础上进行操作,这个含义对于内存控制很有帮助。如果传递的是普通变量,则在函数内获取的是这个变量内存的copy,增加一份新的内存,对于如何使用,建议根据环境视情况而定。

对于函数返回值,可以看下面的示例:

 // 排序函数
func SortFuncPtr(l *[]int) (k *[]int) {
   k = l // k被返回值定义了类型,表示已经声明了,所以此处可以直接赋值
   if len(*k) < 2 {
   } else{
      sort.Ints(*k) // 对l进行排序
   }
   return // 由于我们在返回值中定义了返回值的名称,所以可以省略return的返回内容
   // return &[]int{4,45, 66} //这个返回也是不会报错的
}  

上面的示例和上一个SortFuncPtr函数效果是一样的,不同之处在于我们在函数返回是定义了一个变量名k,Golang中可以对返回类型进行命名,表示返回的是这个变量,可以称为具名返回值, 具名返回参数不管个数多少个,都需要使用圆括号 ( ) 将所有的具名返回参数(包括一个)与对应的数据类型包括起来 ,同时return可以省略返回值内容,但是,如果返回其他非定义的变量的值,只要类型相同也是可以的

Golang中函数可以被作为参数进行传递:

 package main

import (
   "fmt"
   "testing"
)

// 函数参数
func execFunc(f func(int) int, x int) int{ // 此处注意,f的类型是func(int) int, func(int) int是一个整体,表示函数和返回值
   fmt.Println("执行函数!")
   return f(x)
}
func TestArgs(t *testing.T) {
   t.Log("函数参数测试")
   // 定义一个函数fa, 传一个int参数并返回参数
   fa := func(b int) int{
      fmt.Println("我是函数fa")
      return b
   }
   fe := execFunc(fa, 12)
   t.Log("fe的值:", fe)
}  

输出:

 === RUN   TestArgs
    func_test.go:15: 函数参数测试
执行函数!
我是函数fa
    func_test.go:22: fe的值: 12
--- PASS: TestArgs (0.00s)
PASS  

函数作为参数要注意是函数本身作为参数,不是函数的执行结果,同时要明确整个函数的结构,如参数的类型,还有返回值的类型,但是我们发现一个函数作为类型会很长,这样写感觉很不友好,也不符合Go简洁的原则,所以我们可以将函数类型提取出来:

 // 定义一个函数类型,来表示此函数
type testFuncT func(int) int

// 函数参数
func execFuncNew(f testFuncT, x int) int{ // 将原来函数类型替换成定义的类型,这样就简洁多了
   fmt.Println("执行函数!")
   return f(x)
}  

可以看到通过定义一个新的函数类型可以简化很大一部分内容。

如果使用结构体作为参数,看如下示例:

 package main

import (
   "fmt"
   "testing"
)
// 学生
type student struct {
   name string
   id int
   class string
}

func readStudent(s student, q *student) {
   fmt.Println("学生s学号:",s.id)
   fmt.Println("学生s姓名:",s.name)
   fmt.Println("学生s班级:",s.class)
   fmt.Println("学生q学号:",s.id)
   fmt.Println("学生q姓名:",s.name)
   fmt.Println("学生q班级:",s.class)
}
// 测试结构体指针
func TestStructPtr(t *testing.T) {
   s1 := student{"小米",20210902,"02-1班"}
   s2 := &student{"小陈",20210901,"02-2班"}
   readStudent(s1, s2)
   t.Log("输出结束")
}  

输出:

 === RUN   TestStructPtr
学生s学号: 20210902
学生s姓名: 小米
学生s班级: 02-1班
学生q学号: 20210902
学生q姓名: 小米
学生q班级: 02-1班
    func_test.go:28: 输出结束
--- PASS: TestStructPtr (0.00s)
PASS  

可以发现,s和q参数一个是变量一个是指针,但是使用方式没区别,这是Golang中的规则, 在调用结构体时,不管是变量还是指针,都可以通过链式调用其成员的值

0x02 — defer

在开发中,我们经常会使用打开文件,建立连接等操作,这些操作都有个关键点,就是打开或者创建后,在使用完毕时都要手动关闭下,假如我们在java等语言中,这个关闭操作可能会在finaly语句中进行关闭。在Golang中,这个操作将变得非常简单,因为在Golang中有个deferred机制,通过defer关键字触发,具体含义是,在声明defer后,当执行到defer语句后,其后紧跟的函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行。我们可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反(栈)。defer语句经常被用于处理成对的操作,如关闭、断开连接、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放,即使由于panic导致异常结束,defer也会在结束前执行。

 package main

import (
   "fmt"
   "io/ioutil"
   "os"
   "testing"
)

func TestDefer(t *testing.T) {
   //通过打开读取文件进行验证defer
   // 操作文件可以通过os库
   f1, err := os.Open("./text.txt")
   if err != nil {
      t.Log("文件打开失败")
   }
   t.Logf("文件句柄:%v", f1)
   // 定义一个defer,将close包含在函数里中,是因为可以获取关闭成功或失败的输出
   // 同时函数可以固化当前状态,即时后续将f1重新赋值了,也不影响文件关闭
   defer func(f1 *os.File) {
      err := f1.Close()
      if err != nil {
         t.Log("文件关闭失败")
      } else {
         t.Log("文件关闭成功")
      }
   }(f1)
   // 如果我们明确知道f1关闭没问题,可以简化defer
   //defer f1.Close()

   text, err := ioutil.ReadAll(f1) //ioutil是对io的部分接口的封装,不然读取文件需要遍历操作
   if err != nil {
      t.Log("文件读取失败")
   } else {
      t.Logf("text类型:%T, 文件内容:%s", text, string(text))
   }
}  

输出:

 === RUN   TestDefer
    method_test.go:17: 文件句柄:&{0xc0000ac240}
    method_test.go:35: text类型:[]uint8, 文件内容:I am text, balabala
    method_test.go:25: 文件关闭成功
--- PASS: TestDefer (0.00s)
PASS  

可以看到输出,文件关闭是在最后输出的,同时我们读取出来的内容类型是[]uint8(等价[]byte)。

0x03 — 总结

函数多返回值中 ,通过_ 来接收不需要使用的返回值,这种约定一定要熟悉, 开发中常常会遇见函数返回多个值。会经常使用 _ 丢弃不想要的返回值。具名返回值并不常见。但是迟早都会遇到,所以了解他们是很重要的。

函数递归,匿名函数,可变参数, recover和panic等高级用法会在后面找时间专门说明。

相关文章