视频来源:B站《golang入门到项目实战 [2022最新Go语言教程,没有废话,纯干货!]》
文章为自己整理的学习笔记,侵权即删,谢谢支持!
函数是go语言中的一级公民,我们把所有的功能单元都定义在函数中,可以重复使用。
函数包含函数的名称、参数列表和返回值类型,这些构成了函数的签名(signature)。
函数在使用之前必须先定义,可以调用函数来完成某个任务。函数可以重复调用,从而达到代码重用。
func function_name( [parameter list] ) [return_types]
{
函数体
}
func
:函数由 func 开始声明function_name
:函数名称,函数名和参数列表一起构成了函数签名。[parameter list]
:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。return_types
:返回类型,函数返回一列值。return_types
是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types
不是必须的。① 定义一个求和函数
func sum(a int, b int) (ret int) {
ret = a + b
return ret
}
② 定义一个比较两个数大小的函数
func compare(a int, b int) (max int) {
if a > b {
max = a
} else {
max = b
}
return max
}
当我们要完成某个任务时,可以调用函数来完成。调用函数要传递参数,如何有返回值可以获得返回值。
例如:对1.2.2两例的函数进行调用
func main() {
s := sum(1, 2)
fmt.Printf("s: %v\n", s)
max := compare(1, 2)
fmt.Printf("max: %v\n", max)
}
运行结果
s: 3
max: 2
函数可以有0或多个返回值,返回值需要指定数据类型,返回值通过return
关键字来指定。
return
可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。go中的函数可以有多个返回值。return
关键字中指定了参数时,返回值可以不用名称。如果return
省略参数,则返回值部分必须带名称return
中也可以强制指定其它返回值的名称,也就是说return
的优先级更高return
中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b
是正确的,但return c=a+b
是错误的。① 没有返回值
package main
import "fmt"
func f1() {
fmt.Printf("我没有返回值")
}
func main() {
f1()
}
运行结果:
我没有返回值
② 有一个返回值
package main
import "fmt"
func sum(a int, b int) (ret int) {
ret = a + b
return ret
}
func main() {
s := sum(1, 2)
fmt.Printf("s: %v\n", s)
}
运行结果:
s: 3
③ 有多个返回值,且在return中指定返回的内容
package main
import "fmt"
func f2() (name string, age int) {
name = "Psych"
age = 18
return name, age
}
func main() {
name, age := f2()
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
}
运行结果:
name: Psych
age: 18
④ 多个返回值,返回值名称没有被使用
package main
import "fmt"
func f3() (name string, age int) {
name = "Psych"
age = 18
return // 等价于return name, age
}
func main() {
name, age := f3()
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
}
运行结果:
name: Psych
age: 18
⑤ return覆盖命名返回值,返回值名称没有被使用
package main
import "fmt"
func f4() (name string, age int) {
n := "Psych" // 重新声明
a := 18
return n, a
}
func main() {
name, age := f4()
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
}
运行结果:
name: Psych
age: 18
Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如return value,exists
、return value,ok
、return value,err
等。
当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。
但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线_
来丢弃这些返回值。例如下面的f1函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量a。
package main
import "fmt"
func f1() (int, int) {
return 1, 2
}
func main() {
_, x := f1()
fmt.Printf("x: %v\n", x)
}
运行结果:
x: 2
ARGS...TYPE
的方式。这时会将...
代表的参数全部保存到一个名为ARGS
的slice中,注意这些参数的数据类型都是TYPE
。声明函数时的参数列表叫做形参,调用时传递的参数叫做实参。
func f1(a int, b int) int {
// 其中a和b为形参
if a > b {
return a
} else {
return b
}
}
func main() {
r := f1(1, 2)
// 其中1和2为实参
fmt.Printf("r: %v\n", r)
}
运行结果:
r: 2
函数值类型参数默认就是值传递,而引用类型参数默认就是引用传递。
int
系列, float
系列, bool
, string
、数组和结构体 structslice
切片、map
、管道 chan
、interface
等都是引用类型不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
&
,函数内以指针的方式操作变量。从效果上看类似引用 。Go语言函数是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。
package main
import "fmt"
func f1(a int) {
a = 200
fmt.Printf("a1: %v\n", a)
}
func main() {
a := 100
f1(a)
fmt.Printf("a: %v\n", a)
}
运行结果:
a1: 200
a: 100
从运行结果可以看到,调用函数f1后,a的值并没有被改变,说明参数传递是拷贝了一个副本,也就是拷贝了一份新的内容进行运算。
map
、slice
、interface
、channel
这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。
package main
import "fmt"
func f1(a []int) {
a[0] = 100
}
func main() {
a := []int{1, 2}
f1(a)
fmt.Printf("a: %v\n", a)
}
运行结果:
a: [100 2]
从运行结果发现,调用函数后,slice内容被改变了。
Go语言可以使用变长参数,有时候并不能确定参数的个数,可以使用变长参数,可以在函数定义语句的参数部分使用ARGS...TYPE
的方式。这时会将...
代表的参数全部保存到一个名为ARGS
的slice中,注意这些参数的数据类型都是TYPE
。
package main
import "fmt"
func f1(args ...int) {
for _, v := range args {
fmt.Printf("v: %v\n", v)
}
}
func f2(name string, ok bool, args ...int) {
fmt.Printf("name: %v\n", name)
fmt.Printf("ok: %v\n", ok)
for _, v := range args {
fmt.Printf("v: %v\n", v)
}
}
func main() {
f1(1, 2, 3)
fmt.Println("------------")
f1(1, 2, 3, 4, 5, 6)
fmt.Println("------------")
f2("Psych", true, 1, 2, 3)
}
运行结果:
v: 1
v: 2
v: 3
------------
v: 1
v: 2
v: 3
v: 4
v: 5
v: 6
------------
name: Psych
ok: true
v: 1
v: 2
v: 3
可以使用type
关键字来定义一个函数类型,语法格式如下:
type fun func(int, int) int
上面语句定义了一个fun函数类型,它也是一种数据类型
这种函数接收两个int类型的参数,并且返回一个int类型的返回值。
在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
下面我们定义两个这样结构的两个函数,一个求和,一个比较大小:
func sum(a int, b int) int {
return a + b
}
func max(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
下面定义一个fun函数类型,把sum和max赋值给它
package main
import "fmt"
type fun func(int, int) int
func sum(a int, b int) int {
return a + b
}
func max(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
func main() {
var f fun
f = sum
fmt.Printf("f的数据类型是:%T\nsum的数据类型是:%T\n", f, sum)
s := f(1, 2)
fmt.Printf("s: %v\n", s)
f = max
m := f(3, 4)
fmt.Printf("m: %v\n", m)
}
运行结果:
f的数据类型是:main.fun
sum的数据类型是:func(int, int) int
s: 3
m: 4
由此我们可以看出函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
因此函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
package main
import "fmt"
func sum(a int, b int) int {
return a + b
}
func myFun(funvar func(int, int) int, num1 int, num2 int) int {
return funvar(num1, num2)
}
func main() {
res := myFun(sum, 20, 30)
fmt.Printf("res: %v\n", res)
}
运行结果:
res: 50
由4.2了解到了函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用。
其实go语言的函数,可以作为函数的参数,传递给另外一个函数,也可作为另外一个函数的返回值返回。
package main
import "fmt"
func sayHello(name string) {
fmt.Printf("Hello,%s", name)
}
func f1(name string, f func(string)) {
f(name)
}
func main() {
f1("golang", sayHello)
}
运行结果:
Hello,golang
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func cal(s string) func(int, int) int {
switch s {
case "+":
return add
case "-":
return sub
default:
return nil
}
}
func main() {
add := cal("+")
r := add(1, 2)
fmt.Printf("r: %v\n", r)
fmt.Println("-----------")
sub := cal("-")
r = sub(100, 50)
fmt.Printf("r: %v\n", r)
}
运行结果:
r: 3
-----------
r: 50
go语言函数不能嵌套,但是在函数内部可以定义匿名函数,实现一下简单功能调用。
所谓匿名函数就是,没有名称的函数。
语法格式如下:
func (参数列表)(返回值)
当然可以既没有参数,可以没有返回值,
匿名函数也可以实现多次调用。
① 方式一:在定义匿名函数时就直接调用
这种方式匿名函数只能调用一次
package main
import "fmt"
func main() {
func(a int, b int) {
max := 0
if a > b {
max = a
} else {
max = b
}
fmt.Printf("max: %v\n", max)
}(1, 2) // 直接调用,自动执行
}
运行结果:
max: 2
② 方式二:将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
package main
import "fmt"
func main() {
max := func(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
i := max(1, 2)
fmt.Printf("i: %v\n", i)
}
运行结果:
i: 2
③ 方式三:全局匿名函数
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
package main
import "fmt"
var Fun1 = func(n1 int, n2 int) int {
// 此时Fun1是一个全局匿名函数
return n1 * n2
}
func main() {
res := Fun1(4, 9)
// 全局匿名函数的使用
fmt.Printf("res: %v\n", res)
}
运行结果:
res: 36
闭包可以理解成定义在一个函数内部的函数。
在本质上,闭包是将函数内部和函数外部连接起来的桥梁,或者说是函数和其引用环境的组合体。
闭包指的是一个函数和与其相关的引用环境组合而成的实体。
简单来说,闭包=函数+引用环境。
package main
import "fmt"
func add() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
func main() {
var f = add()
fmt.Println(f(10))
fmt.Println(f(20))
fmt.Println(f(30))
fmt.Println("-----------")
f1 := add()
fmt.Println(f1(40))
fmt.Println(f1(50))
}
代码解释说明:
add()
是一个函数,返回的数据类型是fun(int)int
闭包说明:
var n int = 10
return func(x int) int {
n = n + x
return n
}
返回的是一个匿名函数,但是这个匿名函数引用到函数外的n
,因此这个匿名函数就和n
形成一个整体,构成闭包
可以这样理解:闭包是类, 函数是操作,n
是字段。函数和它使用到 n
构成闭包。
当我们反复的调用 f
或者 f1
函数时,因为 n
是初始化一次,因此每调用一次就进行累计。
要搞清楚闭包的关键,就是要分析出返回的函数使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。
在 f
的生命周期内,变量 n
一直有效。
package main
import (
"fmt"
)
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2))
fmt.Println(f1(3), f2(4))
fmt.Println(f1(5), f2(6))
}
运行结果:
11 9
12 8
13 7
请编写一个程序,具体要求如下
makeSuffix(suffix string)
可以接收一个文件后缀名(比如.jpg
),并返回一个闭包.jpg
) ,则返回 文件名.jpg
, 如果已经有.jpg
后缀,则返回原文件名。strings.HasSuffix
, 该函数可以判断某个字符串是否有指定的后缀。package main
import (
"fmt"
"strings"
)
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
// 若 name 没有指定后缀则加上,否则返回原来的名字
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
f := makeSuffixFunc(".jpg")
fmt.Printf("文件名处理后: %v\n", f("winter"))
fmt.Printf("文件名处理后: %v\n", f("bird.jpg"))
}
代码解释说明:
makeSuffix (suffix string)
的 suffix
变量 组合成一个闭包,因为 返回的函数引用到 suffix
这个变量.jpg
,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。函数内部调用函数自身的函数称为递归函数
使用递归函数最重要的三点:
① 阶乘
package main
import "fmt"
func a(n int) int {
// 返回条件
if n == 1 {
return 1
} else {
// 自己调用自己
return n * a(n-1)
}
}
func main() {
n := 5
// 5! = 5x4x3x2x1
r := a(n)
fmt.Printf("r: %v\n", r)
}
运行结果:
r: 120
② 斐波那契数列
斐波拉契数列的计算公式为f(n)=f(n-1)+f(n-2)
且f(2)=f(1)=1
package main
import "fmt"
func f(n int) int {
// 退出点判断
if n == 1 || n == 2 {
return 1
}
// 递归表达式
return f(n-1) + f(n-2)
}
func main() {
r := f(5)
fmt.Printf("r: %v\n", r)
}
运行结果:
r: 5
return
,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该递归本身也会被系统销毁有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有 1 个桃子了。问题:最初共多少个桃子?
思路分析:
package main
import (
"fmt"
)
func peach(n int) int {
if n > 10 || n < 1 {
fmt.Println("天数错误")
return 0 // 返回0表示没有得到正确数量
}
if n == 10 {
return 1
} else {
return (peach(n+1) + 1) * 2
}
}
func main() {
a := peach(1)
fmt.Printf("第一天最初的桃子数量是: %v\n", a)
}
运行结果:
第一天最初的桃子数量是: 1534
go语言中的defer语句
会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
defer的特性:
defer
用于注册延迟调用。return
前才被执。因此,可以用来做资源清理。defer
语句,按先进后出的方式执行。defer
语句中的变量,在defer
声明时就决定了。例如查看执行顺序
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("step1")
defer fmt.Println("step2")
defer fmt.Println("step3")
fmt.Println("end")
}
运行结果:
start
end
step3
step2
step1
当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。
当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制)
在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。请看一段代码:
package main
import (
"fmt"
)
func sum(n1 int, n2 int) int {
//当执行到defer时,暂时不执行,会将defer后面的语句先压入到一个独立的栈
//当函数执行完毕时,再从defer的栈按照先入后出的原则出栈并执行
defer fmt.Println("ok1 n1 = ", n1) // n1=10
defer fmt.Println("ok2 n2 = ", n2) // n2=20
n1++ //n1=11
n2++ //n2=21
res := n1 + n2 //res=32
fmt.Printf("ok3 res= %v\n", res)
return res
}
func main() {
res := sum(10, 20)
fmt.Printf("res= %v\n", res) //res=32
}
运行结果:
ok3 res= 32
ok2 n2 = 20
ok1 n1 = 10
res= 32
defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。
看下模拟代码:
func test(){
//关闭文件资源
file = openfile(文件名)
defer file.close()
// 其他代码
}
func test(){
//释放数据库资源
connect = openDatabse()
defer connect.close()
// 其他代码
}
说明:
defer file.Close()
或者 defer connect.Close()
golang有一个特殊的函数init函数,先于main函数执行,实现包级别的一些初始化操作。
主要特点:
init
函数先于main
函数自动执行,不能被其他函数调用init
函数没有输入参数、返回值init
函数init
函数,这点比较特殊init
执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序init
函数按照包导入的依赖关系决定执行顺序变量初始化
->init()
->main()
package main
import "fmt"
var i int = initVar()
func init() {
fmt.Println("init2")
}
func init() {
fmt.Println("init...")
}
func initVar() int {
fmt.Println("initVar...")
return 100
}
func main() {
fmt.Println("main....")
}
运行结果:
initVar...
init2
init...
main....