【Golang】函数(一等公民)的使用


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

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

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

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

函数的声明(定义)

函数构成了代码执行的逻辑结构,函数的基本组成为:

  • 关键字func
  • 函数名
  • 参数列表
  • 返回值
  • 函数体
  • 返回语句

Go语言拥有三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者lambda函数
  • 方法

普通函数声明

函数声明包括函数名、形式参数列表、返回值列表(可以省略)以及函数体

func 函数名(形式参数列表)(返回值列表){
    函数体
}

例如:

package main

import "fmt"
//声明一个函数
func add(a, b int) (c int) {
    
    
   c = a + b
   return // 此处因为返回值声明了变量名 所以return之后没有带变量名
}
func main() {
    
    
   fmt.Println(add(1, 2)) //调用  结果为3
}
  • 形式参数列表描述了函数的参数名以及类型,是局部变量
  • 返回值列表描述了函数返回值的变量名以及类型
    如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的(即如果返回值声明了变量名,则必须带括号,如果不需要声明返回的变量名,或者是一个没有返回值 则可以省略)

如果一个函数在声明时,包含返回值列表,那么该函数必须以return结尾,除非函数无法运行到函数结尾。

如果一组行参或返回值拥有相同的类型,则可以省略为以下形式

func add(a int, b int) (c int) {
    
    
   c = a + b
   return
}

省略为:

//声明一个函数
func add(a, b int) (c int) {
    
    
   c = a + b
   return // 此处因为返回值声明了变量名 所以return之后没有带变量名
}

函数的类型被称为函数的标识符,如果两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型和标识符(形参和返回值的变量名不影响函数标识符)

函数在调用的时候都必须按照声明的顺序为所有参数提供实参。

在函数中,实参通过值传递的方式进行传递,函数的形参是实参的拷贝、对形参的修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、func、channel等类型,实参可能会由于函数的间接引用被修改。

函数的返回值

Go语言支持多返回值,例如:

conn, err := connectToNetwork()

在这段代码中,connectToNetwork 返回两个参数,conn 表示连接对象,err 返回错误信息。

同一类型的返回值

可以使用一个括号将多个返回值类型括起来,用逗号分隔每个返回值的类型,使用return语句返回时,值列表的顺序需要与函数声明的返回值的类型一致 。

package main

import "fmt"

func getTwoNum() (int, int) {
    
    
   return 1, 2
}
func main() {
    
    
   a, b := getTwoNum()

   fmt.Println(a, b)
}

输出:

1,2

纯类型的返回值对于代码可读性不是很友好

带有变量名的返回值
Go语言支持对返回值进行命名,命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等

例如:

package main

import "fmt"

func getTwoNum() (a, b int) {
    
    
   a = 1
   b = 2
   return
}
func main() {
    
    
   a, b := getTwoNum()

   fmt.Println(a, b)
}

需要注意的是同一种类型返回值和命名返回值两种形式只能选择一种,不可以进行混合使用

例如:

func getTwoNum() (a, b int, int) {
    
    
   a = 1
   b = 2
   return a, b, 0
}
syntax error: mixed named and unnamed parameters

调用函数

Go语言的函数调用格式如下:

返回值变量列表 = 函数名(参数列表)

下面是对各个部分的说明:

  • 函数名:需要调用的函数名。
  • 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
  • 返回值变量列表:多个返回值使用逗号分隔

Go语言变量

函数也是一种数据类型,可以像其他变量一样保存在变量中。

package main

import "fmt"

func sayHello() {
    
    
   fmt.Println("hello")
}

func main() {
    
    
   var f func() //定义一个函数变量
   f = sayHello
   f() //调用
}

匿名函数

没有函数名,只有函数体,函数可以作为一种类型被赋值给函数类型的变量,往往以变量的方式传递。可以在代码里随时的定义匿名函数

定义一个匿名函数

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

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

即没有名称的普通函数

在定义是调用匿名函数
可以在定义完匿名函数后直接调用匿名函数

package main

import "fmt"

func main() {
    
    
   func(data int) {
    
    
      fmt.Println("hello", data)
   }(100)
}

输出:


hello 100

需要注意的是,匿名函数只能定义代码块中,因为如果不定义在代码块中将无法引用,

** 将匿名函数赋值给变量**

匿名函数可以被赋值,例如:

func main() {
    
    
   f := func(data int) {
    
    
      fmt.Println("hello", data)
   }

   f(200)
}

输出:

hello 200

使用函数类型实现接口

函数和其他类型一样属于“一等公民”,其他类型能够实现接口,函数也可以。

— 以后补充

Go语言闭包(Closure)——引用了外部变量的匿名函数

自由(未绑定到特定对象)变量

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:

函数 + 引用环境 = 闭包

同一个函数与不同引用环境组合可以形成不同的实例

函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译静态的概念,闭包是运行期动态的概念。

闭包(Closure)在某些编译软件中也被称为Lambda表达式

闭包对环境中变量的引用过程可以被称为“捕获”,分为引用和复制两种类型。

在闭包内部修改引用的变量

package main

import "fmt"

func main() {
    
    
   //准备一个字符串
   str := "hello world"

   //创建一个匿名函数
   foo := func() {
    
    

      //匿名函数中访问str
      str = "hello golang"
   }

   foo()

   fmt.Println(str)

}

输出:

hello golang

在你名函数中没有定义str,str的定义在匿名函数之前,此时,str被引用到了匿名函数中形成了闭包。

闭包的记忆效应

package main

import (
   "fmt"
)

// Accumulate /* 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
    
    

   // 返回一个闭包
   return func() int {
    
    

      // 累加
      value++

      // 返回一个累加值
      return value
   }
}

func main() {
    
    

   // 创建一个累加器, 初始值为1 返回的是一个匿名函数 
   accumulator := Accumulate(1)

   // 累加1并打印
   fmt.Println(accumulator())

   fmt.Println(accumulator())

   // 打印累加器的函数地址
   fmt.Printf("%p\n", &accumulator)

   // 创建一个累加器, 初始值为1
   accumulator2 := Accumulate(10)

   // 累加1并打印
   fmt.Println(accumulator2())

   // 打印累加器的函数地址
   fmt.Printf("%p\n", &accumulator2)
}

结果:

2
3
0xc00012c018
11
0xc00012c028

代码说明如下:

  • 第 8 行,累加器生成函数,这个函数输出一个初始值,调用时返回一个为初始值创建的闭包函数。
  • 第 11 行,返回一个闭包函数,每次返回会创建一个新的函数实例。
  • 第 14 行,对引用的 Accumulate 参数变量进行累加,注意 value 不是第 11 行匿名函数定义的,但是被这个匿名函数引用,所以形成闭包。
  • 第 17 行,将修改后的值通过闭包的返回值返回。
  • 第 24 行,创建一个累加器,初始值为 1,返回的 accumulator 是类型为 func()int 的函数变量。
  • 第 27 行,调用 accumulator() 时,代码从 11 行开始执行匿名函数逻辑,直到第 17 行返回。
  • 第 32 行,打印累加器的函数地址。

对比输出的日志发现 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例

可变参数类型

可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型:


func myfunc(args ...int) {
    
    
    for _, arg := range args {
    
    
        fmt.Println(arg)
    }
}

上面这段代码的意思是,函数 myfunc() 接受不定数量的参数,这些参数的类型全部是 int,所以它可以用如下方式调用:

myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)

形如 ...type 格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用,通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的可能。

从内部实现机理上来说,类型 ...type 本质上是一个数组切片,也就是 []type ,这也是为什么上面的参数 args 可以用 for 循环来获得每个传入的参数

任意类型的可变参数

之前的例子中将可变参数类型约束为 int,如果你希望传任意类型,可以指定类型为 interface{},下面是Go语言标准库中 fmt.Printf() 的函数原型:

func Printf(format string, args ...interface{
    
    }) {
    
    
    // ...
}

在多个可变参数函数中传递参数

可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加 ... ,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身

Go语言defer(延迟执行语句)

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

相当于java中的finally

多个延迟执行语句的处理顺序

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)

package main

import "fmt"

func main() {
    
    

   fmt.Println("defer begin")

   // 将defer放入延迟调用栈
   defer fmt.Println(1)

   defer fmt.Println(2)

   // 最后一个放入, 位于栈顶, 最先调用
   defer fmt.Println(3)

   fmt.Println("defer end")
}

输出:

defer begin
defer end
3
2
1

发生宕机的情况下,也会发生延迟调用

处理运行时错误

Go语言的错误处理思想及设计包含以下特性:

  • 一个可能造成错误的函数,需要返回值中返回一个错误接口(error),如果调用是成功的,错误接口将返回nil,否则返回错误
  • 在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理

错误接口的定义格式

error 是 Go 系统声明的接口类型,代码如下:


type error interface {
    
    
    Error() string
}

所有符合 Error() string 格式的方法,都能实现错误接口,Error() 方法返回错误的具体描述,使用者可以通过这个字符串知道发生了什么错误、

** 自定义一个错误**

返回错误前,需要定义会产生哪些可能的错误,在Go语言中,使用 errors 包进行错误的定义,格式如下:

var err = errors.New("this is an error") //error类型

错误字符串由于相对固定,一般在包作用域声明,应尽量减少在使用时直接使用 errors.New 返回

errors包
errors 中对 New 的定义非常简单

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
//创建错误对象
func New(text string) error {
    
    
   return &errorString{
    
    text}
}

// errorString is a trivial implementation of error.
//错误字符串
type errorString struct {
    
    
   s string
}

//返回发生何种错误
func (e *errorString) Error() string {
    
    
   return e.s
}

猜你喜欢

转载自blog.csdn.net/qq_45795744/article/details/125749004