函数
函数的基本概念
- 为完成某一功能的程序指令(语句)的集合,称为函数。
- 在 Go 中,函数分为: 自定义函数、系统函数。
func 函数名(形参列表)(返回值列表){
执行语句...
return 返回值列表
}
1) 形参列表: 表示函数的输入 2) 函数中的语句: 表示为了实现某一功能代码块 3) 函数可以有返回值,也可以没有
//将计算的功能,放到一个函数中,然后在需要使用,调用即可
//为了让其它包的文件使用Cal函数,需要将C大小类似其它语言的public
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 '/':
res = n1 / n2
default:
fmt.Println("操作符号错误...")
}
return res
}
包的引出
- 包的本质实际上就是创建不同的文件夹,来存放程序文件。
- go 的每一个文件都是属于一个包的,也就是说 go 是以包的形式来管理文件和项目目录结构的.
包的三大作用
1) 区分相同名字的函数、变量等标识符 2) 当程序文件很多时,可以很好的管理项目 3) 控制函数、变量等访问范围,即作用域
打包基本语法
package 包名
1) 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。 2) 当一个文件要使用其它包函数或变量时,需要先引入对应的包.
//引入方式 1:
import "包名"
//引入方式 2:
import (
"包名"
"包名"
)
//package 指令在 文件第一行,然后是 import 指令。
3) 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的 public ,这样才能跨包访问. 4) 在访问其它包函数,变量时,其语法是 包名.函数名. 5) 如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了 6) 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义 7) 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就是一个语法规范,如果你是写一个库 ,包名可以自定义
函数的调用机制
1) 在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其它的栈的空间区分开来 2) 在每个函数对应的栈中,数据空间是独立的,不会混淆 3) 当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间。
return语句
- 基本语法和说明
func 函数名(形参列表) (返回值类型列表) {
语句...
return 返回值列表
}
1) 如果返回多个值,在接收时希望忽略某个返回值,则使用_
符号表示占位忽略
2) 如果返回值只有一个,(返回值类型列表)
可以不写()
函数的递归调用
- 一个函数在函数体内又调用了本身,我们称为递归调用。
package main
import (
"fmt"
)
func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println("n=", n)
}
func test2(n int) {
if n > 2 {
n-- //递归必须向退出递归条件逼进,否则就是无限循环调用
test2(n)
} else {
fmt.Println("n=", n)
}
}
func main() {
//看一段代码
//test(4) // ?通过分析来看下递归调用的特点
test2(4) // ?通过分析来看下递归调用的特点
}
函数递归需要遵守的重要原则:
1) 执行一个函数时,就创建一个新的受保护的独立空间(新函数栈) 2) 函数的局部变量是独立的,不会相互影响 3) 递归必须向退出递归的条件逼近,否则就是无限递归,死龟了:) 4) 当一个函数执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时,当函数执行完毕或者返回时,该函数本身也会被系统销毁
函数使用的注意事项和细节
1) 函数的形参列表可以是多个,返回值列表也可以是多个。
2) 形参列表和返回值列表的数据类型可以是值类型和引用类型。
3) 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 privat
4) 函数中的变量是局部的,函数外不生效
5) 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
6) 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。
7) Go 函数不支持函数重载
8) 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
9) 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
10) 为了简化数据类型定义,Go 支持自定义数据类型
golang
//基本语法:
type 自定义数据类型名 数据类型
// 理解: 相当于一个别名案例:type myInt int
// 这时 myInt 就等价 int 来使用了
11) 支持对函数返回值命名
12) 使用 _ 标识符,忽略返回值
13) Go 支持可变参数,如果有多个参数,可变参数需要放在形参列表末尾
```golang
// 支持0到多个参数
func sum(args... int) sum int {
}
// 支持1到多个参数
func sum(n int,args... int) sum int {
}
```
init函数
- 每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用。
inti 函数的注意事项和细节
1) 如果一个文件同时包含全局变量定义,init 函数和 main 函数,则执行的流程全局变量定义->init 函数->main 函数 2) init 函数最主要的作用,就是完成一些初始化的工作
匿名函数
- Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
1) 在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。 ```golang package main import ( "fmt" )
func main() {
//在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
//案例演示,求两个数的和, 使用匿名函数的方式完成
res1 := func (n1 int, n2 int) int {
return n1 + n2
}(10, 20)
fmt.Println("res1=", res1)
}
```
2) 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数 ```golang package main import ( "fmt" )
func main() {
//将匿名函数func (n1 int, n2 int) int赋给 a变量
//则a 的数据类型就是函数类型 ,此时,我们可以通过a完成调用
a := func (n1 int, n2 int) int {
return n1 - n2
}
res2 := a(10, 30)
fmt.Println("res2=", res2)
res3 := a(90, 30)
fmt.Println("res3=", res3)
}
```
3) 如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。 ```golang package main import ( "fmt" )
var (
//fun1就是一个全局匿名函数
Fun1 = func (n1 int, n2 int) int {
return n1 * n2
}
)
func main() {
//全局匿名函数的使用
res4 := Fun1(4, 9)
fmt.Println("res4=", res4)
}
```
闭包
- 基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
package main
import (
"fmt"
)
//累加器
func AddUpper() func (int) int {
var n int = 10
var str = "hello"
return func (x int) int {
n = n + x
str += string(36) // => 36 = '$'
fmt.Println("str=", str) // 1. str="hello$" 2. str="hello$$" 3. str="hello$$$"
return n
}
}
func main() {
//使用前面的代码
f := AddUpper()
fmt.Println(f(1))// 11
fmt.Println(f(2))// 13
fmt.Println(f(3))// 16
}
1) AddUpper 是一个函数,返回的数据类型是 fun (int) int
2) 闭包的说明
golang
var n int = 10
return func (x int) int {
n + x
return n
}
// 返回的是一个匿名函数, 但是这个匿名函数引用到函数外的n ,
//因此这个匿名函数就和n 形成一个整体,构成闭包。
3) 大家可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。
4) 当我们反复的调用 f 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。
5) 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。
6) 对上面代码的一个修改,加深对闭包的理解
```golang
package main
import (
"fmt"
"strings"
)
//累加器
func AddUpper() func (int) int {
var n int = 10
var str = "hello"
return func (x int) int {
n = n + x
str += string(36) // => 36 = '$'
fmt.Println("str=", str) // 1. str="hello$" 2. str="hello$$" 3. str="hello$$$"
return n
}
}
func main() {
//使用前面的代码
f := AddUpper()
fmt.Println(f(1))// 11
fmt.Println(f(2))// 13
fmt.Println(f(3))// 16
}
```
函数的defer
- 在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。
package main
import (
"fmt"
)
func sum(n1 int, n2 int) int {
//当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
//当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
defer fmt.Println("ok1 n1=", n1) //defer 3. ok1 n1 = 10
defer fmt.Println("ok2 n2=", n2) //defer 2. ok2 n2= 20
res := n1 + n2 // res = 32
fmt.Println("ok3 res=", res) // 1. ok3 res= 32
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res) // 4. res= 32
}
1) 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[我为了讲课方便,暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。 2) 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制). 3) 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈
package main
import (
"fmt"
)
func sum(n1 int, n2 int) int {
//当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
//当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
defer fmt.Println("ok1 n1=", n1) //defer 3. ok1 n1 = 10
defer fmt.Println("ok2 n2=", n2) //defer 2. ok2 n2= 20
//增加一句话
n1++ // n1 = 11
n2++ // n2 = 21
res := n1 + n2 // res = 32
fmt.Println("ok3 res=", res) // 1. ok3 res= 32
return res
}
func main() {
res := sum(10, 20)
fmt.Println("res=", res) // 4. res= 32
}
- defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。
1) 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close() defer connect.Close() 2) 在 defer 后,可以继续使用创建资源. 3) 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源. 4) 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心
函数参数传递方式
- 两种传递方式 1) 值传递 2) 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
- 值类型和引用类型 1) 值类型:基本数据类型 int系列, float系列, bool, string 、数组 和 结构体struct 2) 引用类型:指针、slice切片、map、管道chan、interface 等都是引用类型
变量作用域
1) 函数内部声明/定义的变量叫局部变量,作用域仅限于函数内部 2) 函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效 3) 如果变量是在一个代码块,比如 for / if 中,那么这个变量的的作用域就在该代码块
内置函数
1) len:用来求长度,比如 string、array、slice、map、channel 2) new:用来分配内存,主要用来分配值类型,比如 int、float32,struct...返回的是指针 3) make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice。
错误处理
- 进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮。
1) Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try…catch…finally 这种处理。 2) Go 中引入的处理方式为:defer, panic, recover 3) 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
使用defer+recover 来处理错误
func test() {
//使用defer + recover 来捕获和处理异常
defer func() {
err := recover() // recover()内置函数,可以捕获到异常
if err != nil { // 说明捕获到错误
fmt.Println("err=", err)
//这里就可以将错误信息发送给管理员....
fmt.Println("发送邮件给admin@sohu.com~")
}
}()
num1 := 10
num2 := 0
res := num1 / num2
fmt.Println("res=", res)
}
func main() {
//测试
test()
for {
fmt.Println("main()下面的代码...")
time.Sleep(time.Second)
}
}
自定义错误
- Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数。
1) errors.New("错误说明") , 会返回一个 error 类型的值,表示一个错误 2) panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序.
//函数去读取以配置文件init.conf的信息
//如果文件名传入不正确,我们就返回一个自定义的错误
func readConf(name string) (err error) {
if name == "config.ini" {
//读取...
return nil
} else {
//返回一个自定义错误
return errors.New("读取文件错误..")
}
}
func test02() {
err := readConf("config2.ini")
if err != nil {
//如果读取文件发送错误,就输出这个错误,并终止程序
panic(err)
}
fmt.Println("test02()继续执行....")
}
func main() {
//测试自定义错误的使用
test02()
fmt.Println("main()下面的代码...")
}