Go语言函数和包

1. 函数介绍

函数是基本的代码块,为完成某一功能的程序指令(语句)的集合,用于执行一个任务

Go语言最少有个main()函数

可以通过函数来划分不同的功能,逻辑上每个函数执行的是指定的任务

函数的声明告诉了编译器函数的名称,返回类型,和参数

Go语言标准库提供了多种可用的内置函数。例如,len()函数可以接受不同类型参数并返回该类型的长度。如果传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。

函数的定义:

1
2
3
4
func function_name ([parameter list]) [return_types]{
函数体
return 返回值列表//函数可以有返回值,也可以没有
}

语法解析:

func:函数由func开始声明

**function_name:**函数的名称,参数列表和返回值类型构成了函数签名

**parameter list:**参数列表,参数就像一个占位符,当函数被调用的时,可以将值传递给参数,这个值被称为实际参数。参数列表指定的事参数类型、顺序、及参数个数。参数是可以选择的,也就是说函数也可以不包括参数。

**return_types:**返回类型,函数返回一列值。return_types是该值的数据类型。有些功能不需要返回值,这种情况下return_types不是必须的。

函数体:函数定义的代码集合。

2. 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

// 输入两个数,在输入一个运算符(+,-,*,/)得到结果
// 函数入门练习
// 定义一个函数
func cal(a1 float64, a2 float64, kay byte) float64 {
var a3 float64
switch kay {
case '+':
a3 = a1 + a2
case '-':
a3 = a1 - a2
case '*':
a3 = a1 * a2
case '/':
if a2 != 0 {
a3 = a1 / a2
} else {
fmt.Println("除数不能为0")
return 0
}
default:
fmt.Println("请正确输入")
return 0
}
return a3
}
func main() {
var a1 float64
var a2 float64
var kay byte
fmt.Println("请输入第一个数a1")
fmt.Scanln(&a1)
fmt.Println("请输入第二个数a2")
fmt.Scanln(&a2)
fmt.Println("请输入+/-/*//")
fmt.Scanf("%c", &kay)
result := cal(a1, a2, kay)
fmt.Println("result=", result)
}

定义一个函数,返回两个值的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func max(num1, num2 int) int {
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}
func main() {
var a int = 100
var b int = 200
var ret int
//调用函数并返回最大值
ret = max(a, b)
fmt.Printf("最大值是:%d\n", ret)
}

3. 函数包

注意⚠️:

  1. 根目录文件夹名称xxx和go mod init xxx 必须要一致
  2. 主文件中import的是子包的目录路径,不能写子包的文件名或者包名。
  3. 调用子包的方法的时候, 前缀必须是子包的包名(package名),和路径或者文件名无关

在实际的开发中,我们往往需要在不同的文件中,去调用其它文件定义的函数,比如 main.go中,去使用 utils.go 文件中的函数,如何实现? 通过包实现

现在有两个程序员共同开发一个 Go 项目,程序员李白希望定义函数 Cal ,程序员杜甫

也想定义函数也叫 Cal。怎么办? 通过包实现

包的基本介绍:

包的本质实际上就是创建不同的文件夹,来存放程序文件。go 的每一个文件都是属于一个包的,也就是说 go 是以包的形式来管理文件和项目目录结构的。

包的使用:

package 包名

import “包的路径”

3.1 包入门案例

创建function-demo根目录,目录结构如下:

image-20240515092419657

生成mod文件

1
go mod init function-demo

image-20240515093614574

uiil.go代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package until

import "fmt"
//func的名字和定义的变量必须大写才能被跨包引用
var Num1 int = 300 //Num必须大写,才能跨包使用
func Cal(n1 float64, n2 float64, operator byte) float64 {
var res float64
switch operator {
case '+':
res = n1 + n2
case '-':
res = n1 - n2
case '*':
res = n1 * n2
case '/':
if n2 != 0 {
res = n1 / n2
} else {
fmt.Println("除数不能为0")
return 0
}
default:
fmt.Println("操作符号错误..")
}
return res
}

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"function-demo/until"
)

func main() {
fmt.Println("until.go Num=", until.Num1)
//输入两个数再输入一个运算符(+,-,*,/),得到结果
var n1 float64 = 1.6
var n2 float64 = 5.6
var operator byte = '+'
result := until.Cal(n1, n2, operator)
fmt.Printf("result=%2.f\n", result)
//输入两个数num1,num2,计算+/*-的值
n1 = 7.8
n2 = 6.7
operator = '*'
result = until.Cal(n1, n2, operator)
fmt.Printf("result=%.2f", result)
}

运行的结果

1
2
3
until.go Num= 300
result=7.20
result=52.26

4. 函数return语句

1
2
3
4
func 函数名(形参列表)[返回值类型列表]{
执行语句 //为了实现某一功能代码块
return 返回值列表 //函数可以有返回值,也可以没有
}
  1. 如果返回多个值,在接收的时候,希望忽略某个返回值。则使用_符号表示占位忽略
  2. 如果返回值只有一个,返回值类型列表可以不写()

4.1 实例

  1. 计算两个数,返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import "fmt"

// 定义一个函数test
func test(n1 int) {
n1 = n1 + 1
fmt.Println("test() n1=", n1)
}

// 定义一个函数getSum
func getSum(n1 int, n2 int) int {
sum := n1 + n2
fmt.Println("getSum sum=", sum)
//当函数有return语句时,就是将结果返回给调用者
//即谁调用我,就返回给谁
return sum
}

// 编写函数,计算两个数的和和差,并返回结果
func getSumAndSub(n1 int, n2 int) (int, int) {
sum := n1 + n2
sub := n1 - n2
return sum, sub
}

func main() {
n1 := 10
test(n1)
fmt.Println("main() n1=", n1)
sum := getSum(10, 20)
fmt.Println("getSum=", sum)
//调用getSumAndSub
res1, res2 := getSumAndSub(1, 2)
fmt.Printf("res1=%v,res2=%v\n", res1, res2)
//希望忽略某个返回值,使用_符号表示占位符
_, res3 := getSumAndSub(9, 6)
fmt.Println("res3=", res3)
}

//最终的输出结果
/*
test() n1= 11
main() n1= 10
getSum sum= 30
getSum= 30
res1=3,res2=-1
res3=3
*/

5. 递归函数

一个函数在函数体内又调用了本身,我们称之为递归调用,语法格式:

1
2
3
4
5
6
7
func recursion(){
recursion()//函数调用自身
}

func main(){
recursion()
}

Go语言支持递归,但是再使用递归时,开发者需要设置退出的条件,否则递归将陷入无限的循环之中。

递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成数列斐波那契数列等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=", n)
}
func main() {
test(4)
}
//最后的输出结果
/*
n= 2
n= 2
n= 3
*/

6. init函数

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

  1. 如果一个文件同时使用包含全局变量定义,init函数和main函数,则执行的流程

    全局变量定义–>init函数–>main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

var age = test()

func test() int {
fmt.Println("test()")
return 90
}
func init() {
fmt.Println("init()...")
}
func main() {
fmt.Println("main()...age=", age)
}

//最终的运行结果
/*
test()
init()...
main()...age= 90
*/
  1. 如果main.go和utils.go中都有变量定义、init函数,执行流程如下:

    utils.go中的变量定义>utils.go中的init函数>main.go中的变量定义>main.go中的init函数>main.go中的main函数

    main.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main

    import (
    "fmt"
    "function-demo2/util"
    )

    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("util的Age=", util.Age)
    fmt.Println("util的Name=", util.Name)
    }

    util.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package util

    import "fmt"

    var Name string
    var Age int

    func init() {
    fmt.Println("util包的init()。。。")
    Age = 100
    Name = "tom~"
    }

    最终输出结果

    1
    2
    3
    4
    5
    6
    util包的init()。。。
    test()
    init()
    main()...age= 90
    util的Age= 100
    util的Name= tom~

6.1 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

// init函数,通常可以在init函数中完成初始化工作
func init() {
fmt.Println("init()...")
}
func main() {
fmt.Println("main()...")
}

//输出的结果
/*
init()...
main()...
*/

7. 匿名函数

go支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以多次调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

var (
fun1 = func(n1 int, n2 int) int {
return n1 * n2
}
)

func main() {
res1 := func(n1 int, n2 int) int {
return n1 + n2
}(10, 10)
fmt.Println("res1=", res1)

a := func(n1 int, n2 int) int {
return n1 - n2
}
res2 := a(10, 10)
fmt.Println("res2=", res2)
res4 := fun1(10, 10)
fmt.Println("res4=", res4)
}

//最终的结果
/*
res1= 20
res2= 0
res4= 100
*/

8. 闭包

闭包就是一个函数与其相关的引用环境组合的一个整体(实体)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

// 累加器
func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
func main() {
f := AddUpper()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println(f(3))

}

//输出的最终结果
/*
11
13
16
*/
  1. AddUpper是一个函数,返回的数据类型是func(int)int
  2. 返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成了一个整体,构成闭包
  3. 当反复的调用f函数时,因为n是初始化的一次,因此每调用一次就进行累计
  4. 要搞清楚闭包的关键,就是要分析出返回的函数和它使用(引用)到哪些变量,因为函数和他引用到的变量共同构成闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"strings"
)

//编写一个函数makeSuffix(suffix string)可以接收一个人间后缀名(比如.jpg),并返回一个闭包
//调用闭包,可以传入一个文件名,如果该文件没有指定的后缀(比如.jgp),则返回文件名.jpg,如果已经有.jpg后缀,则返回原文件名
//要求以闭包的方式完成
//strings.HasSuffix,该函数可以判断某个字符串是否有指定的后缀

func makeSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
f2 := makeSuffix(".jpg")
fmt.Println("文件处理后=", f2("winter"))
fmt.Println("文件处理后=", f2("bird.jpg"))
}

//最后运行的结果
/*
文件处理后= winter.jpg
文件处理后= bird.jpg
*/
  1. makeSuffix(suffix string) func(string) string
    • 这个函数接受一个 suffix(后缀)作为参数,并返回另一个函数。返回的这个函数本身接受一个字符串作为参数并返回一个字符串。
  2. makeSuffix 内部,返回的函数检查提供的字符串(name)是否以 suffix 结尾。如果不是,它会添加后缀;否则,它返回字符串本身。
  3. main 函数中:
    • f2 被赋值为 makeSuffix(".jpg") 返回的函数。这意味着 f2 现在是一个函数,如果字符串不以 “.jpg” 结尾,它将添加 “.jpg”。
    • fmt.Println("文件处理后=", f2("winter")) 打印 “文件处理后=winter.jpg”。由于 “winter” 不以 “.jpg” 结尾,所以添加了后缀。
    • fmt.Println("文件处理后=", f2("bird.jpg")) 打印 “文件处理后=bird.jpg”。由于 “bird.jpg” 已经以 “.jpg” 结尾,所以返回字符串未改变。

9. 函数defer

在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时释放资源,Go的设计者提供defer(延迟机制)

  1. 当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,然后继续执行函数的下一个语句
  2. 当函数执行完毕后,再从defer栈中,一次从栈顶取出语句执行(遵循栈先入后出的机制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func sum(n1 int, n2 int) int {
defer fmt.Println("ok1 n1=", n1)
defer fmt.Println("ok2 n2=", n2)
n1++
n2++
res := n1 + n2
fmt.Println("ok3 res=", res) //32
return res
}
func main() {
res := sum(10, 20)
fmt.Println(res) //32
}

/*
ok3 res= 32
ok2 n2= 20
ok1 n1= 10
32
*/

9.1 defer的最佳实践

defer最主要的价值是,当函数执行完毕后,可以及时的释放函数创建的资源。

image-20240515162239208

说明:

  1. 在golang编程过程中通常的做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者锁资源),可以执行defer file.Close() defer connect.Close()
  2. 在defer后,可以继续使用创建资源
  3. 当函数完毕后,系统回一次从defer栈中,取出语句,关闭资源
  4. 这种机制,非常简洁,程序员不用再为什么时候关闭资源而烦心

10. 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func test() {
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
}
func main() {
test()
fmt.Println("main()下面的代码是。。。")
}

错误总结:

  1. 在默认情况下,当发生错误后(panic),程序就会退出(崩溃)
  2. 但是我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件、短信),这就需要对错误进行特殊的处理

10.1 使用defer+recover来处理错误

进行错误处理后,程序不会轻易挂掉,如果加入预警代码,可以让程序更加的健壮

go语言追求简洁优雅,所以go语言不支持传统的try…catch…finally来处理

go中引用的处理方式为:defer、panic、recover

这几个异常的使用场景可以这么简单的描述:go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"time"
)

func test() {
defer func() {
err := recover()
if err != nil {
fmt.Println("err=", err)
fmt.Println("发送邮件给admin")
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println(res)
}
func main() {
test()
for {
fmt.Println("main()下面的函数")
time.Sleep(time.Second * 10)
}
}
/*
err= runtime error: integer divide by zero
发送邮件给admin
main()下面的函数
*/

10.2 自定义错误

go程序中,也支持自定义错误,使用errors.New和panic内置函数错误

  1. errors.New(“错误说明”),会返回一个error类型的值,表示一个错误
  2. panic内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。可以接收error类型的变量,输出错误信息,并退出程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"errors"
"fmt"
)

// 函数去读取以配置文件init.conf的信息
// 如果文件名传入不正确,我们就返回余个自定义的错误
func readConf(name string) (err error) {
if name == "config.ini" {
return nil
} else {
return errors.New("读取文件错误。。。")
}
}
func test02() {
err := readConf("config.ini")
if err != nil {
panic(err)
}
fmt.Println("test02继续执行")
}
func main() {
test02()
fmt.Println("main()下面的代码")
}
/*
test02继续执行
main()下面的代码
*/

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!