欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

GO语言 函数

程序员文章站 2022-07-14 20:26:01
...

函数 Golang语言

函数

1 函数定义

在go语言中函数定义格式如下:

func functionName([parameter list]) [returnTypes]{
   //body
}
函数由func关键字进行声明。
functionName:代表函数名。
parameter list:代表参数列表,函数的参数是可选的,可以包含参数也可以不包含参数。
returnTypes:返回值类型,返回值是可选的,可以有返回值,也可以没有返回值。
body:用于写函数的具体逻辑

函数的定义

函数构成代码执行的基本逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。

func add(a int,b int)(ret int,err error){
    if (a < 0 || b < 0){
        err = errors.New("should be non-negative number!")
        return
    }
    return a+b,nil
}

如果参数列表中的若干相邻的参数类型相同,如上例中的a和b,则可以在参数列表中省略前面变量的类型声明:

   func add(a,b int)(ret int,err error){
        ///...
    }

Go语言函数有一些限制:

  • 无须前置声明。
  • 不支持命名嵌套定义。
  • 不支持同名函数重载。
  • 不支持默认参数。
  • 支持不定长变参。
  • 支持多返回值。
  • 支持命名返回值。
  • 支持匿名函数和闭包。

函数中,左花括号不能另起一行。
如:

func test()
{               //错误,Go语言规定函数左括号不能在新的一行开头
}

Go语言中函数不能嵌套:

func main(){
    func test(a,b int ,err error){    
    //错误,函数不支持嵌套操作
        ...
    }
}

例1:

下面的函数是用于求两个数的和

func GetSum(num1 int, num2 int) int {
	result := num1 + num2
	return result
}

这个函数传递了两个参数,分别为num1与num2,并且他们都为int类型,将相加后的结果进行返回。

上面这个函数还可以这样定义

func GetSum1(num1, num2 int) int {
	result := num1 + num2
	return result
}

当num1和num2是相同类型的时候我们可以省略掉前面的类型,go编译器会自动进行推断。

2 值传递与引用传递

因为在go语言中存在值类型与引用类型,所以在函数参数进行传递时也要注意这个问题。

  • 值传递是指在函数调用过程中将实参拷贝一份到函数中,这样在函数中如果对参数进行修改,将不会影响到实参。
  • 引用传递是指在函数调用过程中将实参的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实参。
  • 如果想要函数可以直接修改参数的值,那么我们可以用指针传递,将变量的地址作为参数传递到函数中。
  • 下面的这个例子为大家演示了以上的几种情况。

例2:

func paramFunc(a int, b *int, c []int) {
	a = 100
	*b = 100
	c[1] = 100

	fmt.Println("paramFunc:")
	fmt.Println(a)
	fmt.Println(*b)
	fmt.Println(c)
}

func main() {
	a := 1
	b := 1
	c := []int{1, 2, 3}
	paramFunc(a, &b, c)

	fmt.Println("main:")
	fmt.Println(a)
	fmt.Println(b)
	fmt.Println(c)
}

程序输出如下

paramFunc:
100
100
[1 100 3]
main:
1
100
[1 100 3]
函数的调用

Go语言函数调用只需要导入该函数所在的包,直接调用:

import "mymath"
c:=mymath.add(1,2)  

Go语言通过函数名字的大小写来显示函数的可见性,小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
这个规则也适用于类型和变量的可见性。

3 变长参数

在go语言中也支持变长参数,但需要注意的是变长参数必须放在函数参数的最后一个,否则会报错。

下面这段代码演示了如何使用变长参数

例3:

func main() {
	slice := []int{7, 9, 3, 5, 1}
	x := min(slice...)
	fmt.Printf("The minimum is: %d", x)
}

func min(s ...int) int {
	if len(s) == 0 {
		return 0
	}
	min := s[0]
	for _, v := range s {
		if v < min {
			min = v
		}
	}
	return min
}

当然上面这段代码直接将切片作为参数也能实现同样的效果,但是变长参数更多的是为了参数不确定的情况,例如fmt包中的Printf函数设计如下:

func Printf(format string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

上面这段代码暂时看不懂也没关系,但是只需要记住,当你想传递给函数的参数不能确定有多少时可以使用变长参数。

函数的参数

Go语言不支持默认值的可选参数,不支持实名实参。调用时,必须按签名顺序指定类型和数量实参,就算以”_”命名的参数也不能忽略。

func test(x,y int,s string,_ bool){
    return nil
}

func main(){
    test(1,2,"abc")
    //错误,"_"命名的参数也不能忽略
}

参数可视作函数局部变量,因此不能在相同的层次定义同名变量。

func add(x,y int) int{
    x:=100    //错误
    ...
}

不管是指针、引用类型,还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存。

被复制的指针会延长目标对象的生命周期,还有可能导致它被分配到堆上。在栈上复制小对象只需要很少的指令,比运行时堆内存分配快。在并发编程的时候,尽量使用不可变对象,可以消除数据同步等麻烦。如果复制成本过高,或者需要修改原状态,直接使用指针更好。

不定参数

变参本质上是一个切片,只能接受一到多个同类型参数,且必须放在列表尾部:

func test(s string,a ...int){
    ...
}

将切片作为变参时,须进行展开操作。如果是数组,必将其转换成切片。

func test(a ...int){
    fmt.Println(a)
}

func main(){
    a := [3]int{10,20,30}
    test(a[:]...)
}

既然变参时切片,那么参数复制的仅是切片本身,并不包括底层数组,也因此可修改原数据。

4 多返回值

go语言中函数还支持一个特性那就是:多返回值。通过返回结果与一个错误值,这样可以使函数的调用者很方便的知道函数是否执行成功,这样的模式也被称为command,ok模式,在我们未来的程序设计中也推荐大家使用这种方式。下面这段代码显示了如何操作多返回值。

4:

func div(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("The divisor cannot be zero.")
	}
	return a / b, nil
}

func main() {
	result, err := div(1, 2)
	if err != nil {
		fmt.Printf("error: %v", err)
		return
	}
	fmt.Println("result: ", result)
}

也可以下面这种模式

func main() {
	if result, err := div(1, 2); err != nil {
		fmt.Printf("error: %v", err)
		return
	} else {
		fmt.Println("result: ", result)
	}
}

注:多返回值需要使用()进行标记。

返回值

有返回值的函数,必须有明确的return终止语句。

函数可以返回多值模式,函数可以返回更多状态,尤其是error模式。

func dic(x,y int)(int,error){
    if y==0 {
        return 0,errors.New("division by zero")
    }

    return x/y,nil

}

5命名返回值

除了上面支持的多返回值,在go语言中还可以给返回值命名,当需要返回的时候,我们只需要一条简单的不带参数的return语句。我们将上面那个除法的函数修改一下

5:

func div(a, b float64) (result float64, err error) {
	if b == 0 {
		return 0, errors.New("被除数不能等于0")
	}
	result = a / b
	return
}

注:即使只有一个命名返回值,也需要使用()括起来。

命名返回值

命名返回值让函数声明更加清晰,同时也会改善帮助文档和代码编辑器提示。

命名返回值和参数一样,可当做函数局部变量使用,最后由return隐式返回。

func dic(x,y int)(z int,err error){
    if y==0 {
        err=errors.New("division by zero")
        return
    }
    z = x/y
    return
}

命名返回值会被不同层级的同名变量屏蔽。编译器可以检查这类错误,只需要显示return返回即可。

func add(x,y int)(z int){
    z:= x + y  //同名局部变量进行了覆盖
    return    //错误,改成return z
}

如果使用命名返回值,则需要全部使用命名返回值。

func test()(int,s string{  
            //错误
    ...            
}

6 匿名函数

匿名函数如其名字一样,是一个没有名字的函数,除了没有名字外其他地方与正常函数相同。匿名函数可以直接调用,保存到变量,作为参数或者返回值。

6:

func main() {
	f := func() string {
		return "hello world"
	}
	fmt.Println(f())
}
匿名函数

匿名函数是指没有定义名字符号的函数。

除了没有名字外,在函数内部定义匿名函数可以形成嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。

func main(){
    func(s string){
        fmt.Println(s)
    }("hellow world")
}

除闭包因素外,匿名函数也是常见的重构的手段。可将大函数分解成多个相对独立的匿名函数块,然后相对简洁的完成调用逻辑流程,实现框架和细节分离。

7 闭包

闭包可以解释为一个函数与这个函数外部变量的一个封装。粗略的可以理解为一个类,类里面有变量和方法,其中闭包所包含的外部变量对应着类中的静态变量。 为什么这么理解,首先让我们来看一个例子。

7:

func add() func(int) int {
	n := 10
	str := "string"
	return func(x int) int {
		n = n + x
		str += strconv.Itoa(x)
		fmt.Print(str, " ")
		return n
	}
}

func main() {
	f := add()
	fmt.Println(f(1))
	fmt.Println(f(2))
	fmt.Println(f(3))

	f = add()
	fmt.Println(f(1))
	fmt.Println(f(2))
	fmt.Println(f(3))
}

程序输出结果如下:

string1 11
string12 13
string123 16
string1 11
string12 13
string123 16
  • 如果不了解的闭包肯定会觉得很奇怪,为什么会输出这样的结果。这就要用到我最开始的解释。闭包就是一个函数和一个函数外的变量的封装,而且这个变量就对应着类中的静态变量。 这样就可以将这个程序的输出结果解释的通了。

  • 最开始我们先声明一个函数add,在函数体内返回一个匿名函数
    其中的n,str与下面的匿名函数构成了整个的闭包,n与str就像类中的静态变量只会初始化一次,所以说尽管后面多次调用这个整体函数,里面都不会再重新初始化了

  • 而且对于外部变量的操作是累加的,这与类中的静态变量也是一致的
    在go语言学习笔记中,雨痕提到在汇编代码中,闭包返回的不仅仅是匿名函数,还包括所引用的环境变量指针,这与我们之前的解释也是类似的,闭包通过操作指针来调用对应的变量。

闭包

Go语言匿名函数就是一个闭包,闭包是可以包含*变量(未绑定到特定变量)的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于*变量包含在代码块中,所以这些*变量以及他们引用的对象没有被释放)为*变量提供绑定的计算环境(作用域)。

func test(x intfunc(){
    return func(){
        println(x)
    }
}

func main(){
    f := test(123)
    f()
}

test返回的匿名函数会引用上下文环境变量x,当main函数执行时,依旧可以读取到x的值。
正因为闭包通过指针引用环境变量,可能导致其生命周期延长,甚至被分配到堆内存。另外,还有”延迟求值”特性。

func test()[]func(){
    var s []func()
    for i := 0;i < 2;i++{
        s = append(s,func(){
            fmt.Println(i)
        })
    }
    return s
}

func main(){
    for _,f := rang test(){
        f()
    }
}

结果是:

    2
    2

在for循环内部复用局部变量i,每次添加的匿名函数引用的是同意变量。添加仅仅把匿名函数放入列表,并未执行。当main函数执行这些函数时,读取的是环境变量i最后循环的值。
修改为:

func test()[]func(){
    var s []func()

    for i := 0;i < 2;i++{
        x := i


//每次用不同的环境变量或传参复制,让各自的闭包环境各不相同。
        s = append(s,func(){
            fmt.Println(x)
        })
    }

    return s
}

小问题:

尝试一下如何通过闭包来实现斐波那契数列。

the way to go小练习——闭包实现斐波那契数列


 func fib() func() int {
       var a int = 0
       var b int = 1
       return func() int {
           c := a
           a = b
           b = a + c
           return c
       } }
   
   func main() {
       f := fib()
       for i := 0; i < 10; i++ {
           fmt.Println(f())
       } }