Golang学习笔记(04-1-函数的定义)

169 阅读7分钟

1. 函数的定义

Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。与Python不同,Go语言的函数定义格式变种比较多,且对形参和返回值要求严格,需要指定其类型,因此产生了很多不同形式的变种!所有语言中函数的都是三要素构成,分别是 函数名称、参数、返回值!

1.1. 函数的通用格式

func funcName(args)(res) {
    funcBody
}
  • func: 函数声明的关键词,类似于Python中def
  • funcName: 函数名称,由字母、数字、下划线组成,不以数字开头,通常采用驼峰结构。在同一个包中函数名称不能重复。main函数为程序的入口函数。
  • args: 形参,可选项。支持一个或者多个参数,也支持不定长参数,但是不支持默认参数。参数需要指定数据类型
  • res: 返回值,可选项。支持一个或者多个返回值,需要指定返回值类型。
  • funcBody: 函数体,即函数内部代码块。

1.2. 函数的各种形式

1.2.1. 不带参数和返回值

package main

import "fmt"

func f1() {
	fmt.Println(100 + 200)
}

func main() {
	f1()
}

1.2.1. 带形参的函数

package main

import "fmt"

func f1(a int, b int, c string) {  // 位置实参类型与形参不一致会报错
	fmt.Println(c, "=", a+b)
}

func main() {
	f1(100, 200, "100+200")
}

1.2.2. 形参简写

package main

import "fmt"

func f1(a, b int, c string) { // 参数a 省略类型表示与后一个参数类型一致
	fmt.Println(c, "=", a+b)
}

func main() {
	f1(100, 200, "100+200")
}

1.2.3. 不定长传参

  • 不定长传参的变量需要作为最后的形参
  • 不定长参数接收参数后,转换为切片,不能与前一个形参采用简写形式
  • 不定长传参可以接收0个实参
package main

import "fmt"

func f1(a int, b ...int) { // 不能用 a, b ...int, 因为b为切片
	fmt.Printf("a:%#v\tb:%#v\n", a, b) //
	var res int
	for _, v := range b {
		res += v
	}
	fmt.Println(res)
}

func main() {
	f1(100, 200, 300, 400, 500)
	f1(1000)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day03\02-define_func>go run main.go
a:100   b:[]int{200, 300, 400, 500}
1400
a:1000  b:[]int(nil)
0

1.2.4. 一个返回值

package main

import "fmt"

func f1(n ...int) int {
	var res int
	for _, v := range n {
		res += v
	}
	return res
}

func main() {
	fmt.Println(f1(100, 200, 300, 400, 500))
}

1.2.5. 多个返回值

当需要返回多个值的时候,可以放到一个序列中,比如数组或者切片中,也可以返回多个值!

package main

import "fmt"

func f1(n int) (int, int) {
	var (
		ret1 int = 0
		ret2 int = 1
	)
	for i := 1; i <= n; i++ {
		ret1 += i
		ret2 *= i
	}
	return ret1, ret2
}

func f2(n int) [2]int {
	var (
		ret1 int = 0
		ret2 int = 1
	)
	for i := 1; i <= n; i++ {
		ret1 += i
		ret2 *= i
	}
	return [2]int{ret1, ret2}
}

func main() {
	ret1, ret2 := f1(15)
	ret3 := f2(15)
	fmt.Printf("f1: ret1:%d\tret2:%d\n", ret1, ret2)
	fmt.Printf("f2: res:%#v\n", ret3)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day03\02-define_func>go run main.go
f1: ret1:120    ret2:1307674368000
f2: res:[2]int{120, 1307674368000}

1.2.6. 命名返回值

当在指定返回值的变量名称时,即完成了对该变量声明,如对数字来说,初始值为0,这个可能会导致计算结果异常!

  • 命名返回值会自动完成变量的声明
  • 必须要使用return语句,否则语法错误。可以仅仅使用一个return
  • 命名返回值可以采用简写
  • 不支持命名返回值和不命名返回值混用
package main

import "fmt"

func f1(n int) (ret1 int, ret2 int) {
	ret2 = 1 // ret2 初始值为0,会导致计算异常
	for i := 1; i <= n; i++ {
		ret1 += i
		ret2 *= i
	}
	return // 等同于 return ret1,ret2
}

func f2(n int) (ret1, ret2 int) {
	ret2 = 1 // ret2 初始值为0,会导致计算异常
	for i := 1; i <= n; i++ {
		ret1 += i
		ret2 *= i
	}
	return ret1, ret2
}

func main() {
	ret1, ret2 := f1(15)
	ret3, ret4 := f2(15)
	fmt.Printf("f1: ret1:%d\tret2:%d\n", ret1, ret2)
	fmt.Printf("f2: ret3:%d\tret4:%d\n", ret3, ret4)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day03\02-define_func>go run main.go
f1: ret1:120    ret2:1307674368000
f2: ret3:120    ret4:1307674368000

1.3. 参数类型

在Go中,无论传递的参数是值类型还是引用类型(参考),都是拷贝一份参数的值到函数内部,但是在使用中,引用类型和值类型的变量是有区别的,引用类型只是拷贝一个内存地址,资源开销小,较为常用。

1.3.1. 值类型的参数

函数内部接收到的参数 s 和 传递的s0 是两个值相同,但是内存地址不同的变量。因此,修改s并不会影响s0。

package main

import "fmt"

func f0(s string)  {
	fmt.Printf("s: type:%T  value:%v  addr:%p\n", s, s, &s)
}

func main()  {
	s0 := "hello world!"
	fmt.Printf("s0: type:%T  value:%v  addr:%p\n", s0, s0, &s0)
	f0(s0)
}
[root@heyingsheng src]# go run studygo/day03/func14/main.go
s0: type:string  value:hello world!  addr:0xc0000461f0
s: type:string  value:hello world!  addr:0xc000046220

1.3.2. 引用类型参数

下面案例中,s1为引用类型,它的内存地址为 0xc000006028 ,存储的是s0的地址 0xc0000461f0 。函数f0中,s的内存地址为 0xc000006038 ,存储的也是s0的地址 0xc0000461f0 。
通过这个案例可以发现,引用类型作为参数时,函数参数接收的也是实参的副本,只不过s1和函数内s指向的都是s0,因此可以通过s去修改s0的值罢了。

package main

import "fmt"

func f0(s *string)  {
	fmt.Printf("s: type:%T  value:%v  addr:%p\n", s, s, &s)
}

func main()  {
	s0 := "hello world!"
	s1 := &s0
	fmt.Printf("s1: type:%T  value:%v  addr:%p\n", s1, s1, &s1)
	f0(s1)
}
[root@heyingsheng src]# go run studygo/day03/func14/main.go
s1: type:*string  value:0xc0000461f0  addr:0xc000006028
s: type:*string  value:0xc0000461f0  addr:0xc000006038

1.3.3. 案例

package main

import "fmt"

func f0(s *string) {
	*s = "HELLO WORLD!"
}

func main() {
	s0 := "hello world!"
	f0(&s0)  // 修改s0的值
	fmt.Println(s0)
}
[root@heyingsheng src]# go run studygo/day03/func14/main.go
HELLO WORLD!

2. 变量的作用域

Go语言中变量的作用域分为三种,全局变量、函数内的局部变量、代码块中的局部变量。变量的检索方式与Python类似,都是从当前代码向上寻找,就近匹配!

package main

import "fmt"

var (
	a string = "global a"
	b string = "global b"
	c string = "global c"
)

func main() {
	a := "func a"
	b := "func b"
	if a := "if a"; true {
		fmt.Printf("if 语句块内部: %#v\t%#v\t%#v\n", a, b, c)
	}
	fmt.Printf("main 函数内部: %#v\t%#v\t%#v\n", a, b, c)
}
e:\OneDrive\Projects\Go\src\gitee.com\studygo\day03\03-vars>go run main.go
if 语句块内部: "if a"   "func b"        "global c"
main 函数内部: "func a" "func b"        "global c"

3. 匿名函数

匿名函数就是在定义的时候没有指定名字的函数,在Go语言中,函内部是无法使用func关键词定义新的命名函数,如果需要定义则必须使用匿名函数的方式。匿名函数的使用方式有两种:

  • 定义匿名函数的时候直接调用,即一次性使用,在函数的defer语句中常用
  • 将函数赋值给变量,实现多次调用

3.1. 函数定义时直接使用

package main

import "fmt"

func main()  {
	// 定义匿名函数并直接使用
	res := func (n1, n2 int) int {
		return n1 + n2
	}(100,200)

	fmt.Println("res=",res) // 300
}

3.2. 匿名函数赋值给变量

package main

import "fmt"

func main()  {
	// 定义匿名函数并赋值给变量
	add := func (n1, n2 int) int {
		return n1 + n2
	}
	res := add(100, 200)
	fmt.Println("res=",res)  // 300
	res = add(300,400)
	fmt.Println("res=",res)  // 700
}

3.3. 全局匿名函数

package main

import "fmt"

// 定义匿名函数并赋值给变量, 几乎不用
var add = func (n1, n2 int) int {
	return n1 + n2
}

func main()  {
	fmt.Printf("%T %v\n", add, add)  // func(int, int) int 0x49fb10
	res := add(100, 200)
	fmt.Println("res=",res)  // 300
	res = add(300,400)
	fmt.Println("res=",res)  // 700
}


4. 小练习

package main

import (
	"fmt"
	"strings"
)

/*
你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配规则如下:
a. 名字中每包含1个'e'或'E'分1枚金币
b. 名字中每包含1个'i'或'I'分2枚金币
c. 名字中每包含1个'o'或'O'分3枚金币
d: 名字中每包含1个'u'或'U'分4枚金币
写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
*/

var (
	coins = 50
	users = [...]string{"Matthew","Sarah","Augustus","Heidi","Emilie","Peter","Giana","Adriano","Aaron","Elizabeth"}
	distribution  = make(map[string]int,len(users))
)

func dispatchCoin() int {
	for _, name := range users{
		ret := strings.Count(name, "e") + strings.Count(name, "E") + 
			(strings.Count(name, "i")+ strings.Count(name, "I")) * 2 +
			(strings.Count(name, "o")+ strings.Count(name, "O")) * 3 +
			(strings.Count(name, "u")+ strings.Count(name, "U")) * 4
		distribution[name] = ret
		coins -= ret
	}
	return coins
}

//func dispatchCoin() int {
//	for _, name := range users{
//		distribution[name] = 0
//		for _, char := range name {  // 循环遍历
//			switch char {
//			case 'e','E':
//				distribution[name] += 1
//				coins -= 1
//			case 'i','I':
//				distribution[name] += 2
//				coins -= 2
//			case 'o','O':
//				distribution[name] += 3
//				coins -= 3
//			case 'u', 'U':
//				distribution[name] += 4
//				coins -= 4
//			}
//		}
//	}
//	return coins
//}

func main() {
	left := dispatchCoin()
	fmt.Println("剩下:", left)
	fmt.Printf("%#v\n", distribution)
}
[root@heyingsheng day03]# go run 10-homework/main.go
剩下: 10
map[string]int{"Aaron":3, "Adriano":5, "Augustus":12, "Elizabeth":4, "Emilie":6, "Giana":2, "Heidi":5, "Matthew":1, "Peter":2, "Sarah":0}