我正在参加「掘金·启航计划」
对于大多数编程语言来说,函数都是很重要的内容,尤其是用面向过程编程语言(比如C
语言)编写的程序,就是由一个个函数构成的,另外,很多编程语言的入口就是一个函数,即main
函数,Go
语言的入口也是main
函数:
package mainfunc main(){//程序入口}package main func main(){ //程序入口 }package main func main(){ //程序入口 }
掌握函数的使用非常重要,在这篇文章中,我们就来学习Go
函数。
什么是函数
函数是一个有指定名称的可以重复使用的逻辑代码块,我们可以将重复或复杂的逻辑代码封装在一个函数中,在之后的代码中重复使用,比如fmt.Println
函数就是Go
标准库为我们封装好的一个函数,我们可以调用该函数向控制台打印信息:
fmt.Println("hello")fmt.Println("hello")fmt.Println("hello")
函数的作用
- 将复杂的逻辑封装成一个函数,多次复用。
- 将一个复杂的任务拆解为多个小任务,由多个简单的函数完成。
- 复用其他开发者封装的函数,或者将自己的代码封装成函数给其他开发者使用。
函数的使用
要使用函数,则必须先声明函数。
函数声明
声明函数用关键字func
,后面跟着函数名称、小括号中的参数列表、返回值列表(可省略)以及花括号中的函数体(可以为空),如下所示:
func functionName([param1 type,param2 type,...paramN type])[(result1 type,result2 type,...resultN type)] {//function body}func functionName([param1 type,param2 type,...paramN type])[(result1 type,result2 type,...resultN type)] { //function body }func functionName([param1 type,param2 type,...paramN type])[(result1 type,result2 type,...resultN type)] { //function body }
函数的参数列表与返回值列表被称为函数签名。
函数声明示例:
//没有参数和返回值func say(){fmt.Println("hello")}//两个参数和一个返回值func sum(a,b int) int {return a+b}//没有参数和返回值 func say(){ fmt.Println("hello") } //两个参数和一个返回值 func sum(a,b int) int { return a+b }//没有参数和返回值 func say(){ fmt.Println("hello") } //两个参数和一个返回值 func sum(a,b int) int { return a+b }
函数调用
函数声明后,就可以调用函数了,如果函数有参数,调用时必须传入参数,Go函数的参数没有默认值。
对于同一个包下的函数,可以直接调用:
package mainimport "fmt"func sum(a,b int)int{return a + b}func main(){fmt.Println(sum(1,2))}package main import "fmt" func sum(a,b int)int{ return a + b } func main(){ fmt.Println(sum(1,2)) }package main import "fmt" func sum(a,b int)int{ return a + b } func main(){ fmt.Println(sum(1,2)) }
如果函数名首字母为大写,则在其他包中可以通过该函数所在包名调用该函数,如果函数小写,则该函数只能在包内调用:
package personfunc Eat(){fmt.Println("I'm eatting")}func say(){fmt.Println("hello")}package person func Eat(){ fmt.Println("I'm eatting") } func say(){ fmt.Println("hello") }package person func Eat(){ fmt.Println("I'm eatting") } func say(){ fmt.Println("hello") }
使用包名调用其他包的函数:
package mainimport "person"func main(){person.Eat()person.say() //错误}package main import "person" func main(){ person.Eat() person.say() //错误 }package main import "person" func main(){ person.Eat() person.say() //错误 }
参数列表
Go函数可以有0
到N
个参数,N
个表示函数的参数是没有限制,可以是10个,也可以是100个,甚至是1000或10000,但一般推荐函数参数最好不要超过3个。
func isOdd(n int) bool {return n % 2 != 0 ? true : false}func sum(a int,b int,c int) int {return a + b + c}func isOdd(n int) bool { return n % 2 != 0 ? true : false } func sum(a int,b int,c int) int { return a + b + c }func isOdd(n int) bool { return n % 2 != 0 ? true : false } func sum(a int,b int,c int) int { return a + b + c }
如果相临的参数据数据类型相同,则前面的参数可以省略数据类型:
func sum(a int,b int,c int) int {return a + b + c}//改为func sum(a,b,c int) int {return a + b + c}func sum(a int,b int,c int) int { return a + b + c } //改为 func sum(a,b,c int) int { return a + b + c }func sum(a int,b int,c int) int { return a + b + c } //改为 func sum(a,b,c int) int { return a + b + c }
函数参数作用域只在函数内,在函数外部无法访问函数的参数:
func sum(a,b int) int {return a+b;}sum(1,2)fmt.Println(a) //错误,无法调用函数的参数func sum(a,b int) int { return a+b; } sum(1,2) fmt.Println(a) //错误,无法调用函数的参数func sum(a,b int) int { return a+b; } sum(1,2) fmt.Println(a) //错误,无法调用函数的参数
有时候,我们希望函数的参数数量是可变,比如我们前面一直调用的打印函数fmt.Println()
,我们可以传入任意数量的参数:
fmt.Println(1,2,3)fmt.Println(1,2,3)fmt.Println(1,2,3)
我们也可以定义自己的可变参数函数,可变参数变量在函数内部就像一个切片:
func sum(vals ...int) int {total := 0for _, val := range vals {total += val}return total}func sum(vals ...int) int { total := 0 for _, val := range vals { total += val } return total }func sum(vals ...int) int { total := 0 for _, val := range vals { total += val } return total }
可变参数变量必须是函数的最后一个参数:
//错误func sum(vals ...int,x int) int {total := 0for _, val := range vals {total += val}return total}//正确func sum(x int,vals ...int) int {total := 0for _, val := range vals {total += val}return total}//错误 func sum(vals ...int,x int) int { total := 0 for _, val := range vals { total += val } return total } //正确 func sum(x int,vals ...int) int { total := 0 for _, val := range vals { total += val } return total }//错误 func sum(vals ...int,x int) int { total := 0 for _, val := range vals { total += val } return total } //正确 func sum(x int,vals ...int) int { total := 0 for _, val := range vals { total += val } return total }
返回值列表
在Go
语言中,函数支持0
到N
个返回值,返回值写在return
关键词后面,多个返回值由逗号隔开:
func sum(a,b int) (n int) {return a+b}func isOdd(n int) (bool,error) {if n == 0{return false,errors.New("param n can not be zero")}if n % 2 != 0 {return true,nil}return false,nil}func sum(a,b int) (n int) { return a+b } func isOdd(n int) (bool,error) { if n == 0{ return false,errors.New("param n can not be zero") } if n % 2 != 0 { return true,nil } return false,nil }func sum(a,b int) (n int) { return a+b } func isOdd(n int) (bool,error) { if n == 0{ return false,errors.New("param n can not be zero") } if n % 2 != 0 { return true,nil } return false,nil }
当函数没有返回值时,或者只有一个返回值且返回值没有名称时,返回值列表的小括号可以省略:
func hello(str string){fmt.Println(str)}func sum(a,b int) int {return a+b}func hello(str string){ fmt.Println(str) } func sum(a,b int) int { return a+b }func hello(str string){ fmt.Println(str) } func sum(a,b int) int { return a+b }
返回值也是函数内部参数,当函数直接操作返回值时,return
关键词后面也可不跟返回值,调用者仍能获得返值:
func sum(a,b int) (n int) {n = a + breturn}fmt.Println(sum(1,2))//输出3func sum(a,b int) (n int) { n = a + b return } fmt.Println(sum(1,2))//输出3func sum(a,b int) (n int) { n = a + b return } fmt.Println(sum(1,2))//输出3
如果有多个返回值,无论返回值是否有名称,都必须写在小括号中:
func test()(a int,b string){return 10,"test"}func test()(a int,b string){ return 10,"test" }func test()(a int,b string){ return 10,"test" }
返回值也可以省略名称,比如上面的函数可以改写为:
func test()(int,string){return 10,"test"}func test()(int,string){ return 10,"test" }func test()(int,string){ return 10,"test" }
在Go语言中,一般推荐第二个返回值为error类型,用于提示调用者该函数在执行过程中,是否有错误,Go标准库的很多函数就是这么做的:
func OpenFile(name string, flag int, perm FileMode) (*File, error) {testlog.Open(name)f, err := openFileNolog(name, flag, perm)if err != nil {return nil, err}f.appendMode = flag&O_APPEND != 0return f, nil}func OpenFile(name string, flag int, perm FileMode) (*File, error) { testlog.Open(name) f, err := openFileNolog(name, flag, perm) if err != nil { return nil, err } f.appendMode = flag&O_APPEND != 0 return f, nil }func OpenFile(name string, flag int, perm FileMode) (*File, error) { testlog.Open(name) f, err := openFileNolog(name, flag, perm) if err != nil { return nil, err } f.appendMode = flag&O_APPEND != 0 return f, nil }
匿名函数
函数作为Go
语言中的一等公民,只能在包一级声明,Go
不支持在函数中再声明其他函数:
package main//正确func sum1(){}func main(){//在函数中声明其他函数,无法通过编译func sum2(){}}package main //正确 func sum1(){ } func main(){ //在函数中声明其他函数,无法通过编译 func sum2(){ } }package main //正确 func sum1(){ } func main(){ //在函数中声明其他函数,无法通过编译 func sum2(){ } }
如果想在其他函数中声明一个函数,可以使用匿名函数,所谓匿名函数,就是省略了函数名的函数,也称为函数字面量:
package main//正常声明函数func getFile(){}func main(){go getFile()//匿名函数go func(){}()}package main //正常声明函数 func getFile(){ } func main(){ go getFile() //匿名函数 go func(){ }() }package main //正常声明函数 func getFile(){ } func main(){ go getFile() //匿名函数 go func(){ }() }
将匿名函数作为函数的参数,也称为闭包:
package mainimport ("io""log""net/http")func main() {http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {io.WriteString(w, "Hello from a HandleFunc #1!\n")})http.HandleFunc("/endpoint",func(w http.ResponseWriter, _ *http.Request) {io.WriteString(w, "Hello from a HandleFunc #2!\n")})log.Fatal(http.ListenAndServe(":8080", nil))}package main import ( "io" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #1!\n") }) http.HandleFunc("/endpoint",func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #2!\n") }) log.Fatal(http.ListenAndServe(":8080", nil)) }package main import ( "io" "log" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #1!\n") }) http.HandleFunc("/endpoint",func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #2!\n") }) log.Fatal(http.ListenAndServe(":8080", nil)) }
匿名函数也可以作为一个值赋给一个变量,比如我们上面的例子可以改写为下面的样子:
package mainimport ("io""log""net/http")func main() {h1 := func(w http.ResponseWriter, _ *http.Request) {io.WriteString(w, "Hello from a HandleFunc #1!\n")}h2 := func(w http.ResponseWriter, _ *http.Request) {io.WriteString(w, "Hello from a HandleFunc #2!\n")}http.HandleFunc("/", h1)http.HandleFunc("/endpoint", h2)log.Fatal(http.ListenAndServe(":8080", nil))}package main import ( "io" "log" "net/http" ) func main() { h1 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #1!\n") } h2 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #2!\n") } http.HandleFunc("/", h1) http.HandleFunc("/endpoint", h2) log.Fatal(http.ListenAndServe(":8080", nil)) }package main import ( "io" "log" "net/http" ) func main() { h1 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #1!\n") } h2 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #2!\n") } http.HandleFunc("/", h1) http.HandleFunc("/endpoint", h2) log.Fatal(http.ListenAndServe(":8080", nil)) }
匿名函数可以作为函数的返回值或者直接用定义函数类型的变量。
defer机制
很多时候,我们的函数需要打开网络连接去读取网络数据,又或者打开文件读取数据,这些操作往往需要在函数的最后面执行对应的关闭操作,不过随着函数代码越写越长,我们可能会忘记关闭打开的资源。
在其他编程语言中(比如Java),会把关闭资源的操作放在try...catch
后面finally
模块中:
try{//执行可能会抛出异常的代码}catch(Exception e){//捕获异常}finally {//执行资源关闭操作}try{ //执行可能会抛出异常的代码 }catch(Exception e){ //捕获异常 }finally { //执行资源关闭操作 }try{ //执行可能会抛出异常的代码 }catch(Exception e){ //捕获异常 }finally { //执行资源关闭操作 }
只是Go
并不支持try...catch
,Go语言的defer
机制可以达到同样的效果,defer关键字后面跟着一个函数调用,当函数执行完成后,会调用defer后面的函数:
func getData() []byte{resp, err := http.Get("http://example.com/")if err != nil {// handle error}defer resp.Body.Close()body, err := io.ReadAll(resp.Body)//.....}func getData() []byte{ resp, err := http.Get("http://example.com/") if err != nil { // handle error } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) //..... }func getData() []byte{ resp, err := http.Get("http://example.com/") if err != nil { // handle error } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) //..... }
函数异常处理
开发函数时,一个好的做法是通过函数第二个返回值为error类型来提供调用者是否出错,由调用者自行决定是否终止程序执行,不过Go也支持在函数中直接抛出异常,以终止程序运行。
Go
抛出异常使用内置panic()
函数,执行该函数后,程序会终止运行:
package mainfunc main() {add(0, 0)}func add(x, y int) int {if x == 0 && y == 0 {panic("x与y不能同时为0")}return x + y}package main func main() { add(0, 0) } func add(x, y int) int { if x == 0 && y == 0 { panic("x与y不能同时为0") } return x + y }package main func main() { add(0, 0) } func add(x, y int) int { if x == 0 && y == 0 { panic("x与y不能同时为0") } return x + y }
一般而言,如果调用函数过程出现了panic
异常,不应该进行错误恢复的,因为panic异常代表程序出现严重的错误了,比如数组越界等。
而有时候, 我们希望在产生panic
异常后做一些善后处理,比如将异常中的错误信息打印到日志等,这时候可以使用内置recover()
函数。
recover()
需要在defer
后面的函数中调用才会生效:
package mainimport "fmt"func main() {caller()}func caller() {defer func() {if p := recover(); p != nil {err := fmt.Errorf("执行出错: %v", p)fmt.Println(err)}}()r1 := add(1, 2)fmt.Println(r1)r2 := add(0, 0)fmt.Println(r2)}func add(x, y int) int {if x == 0 && y == 0 {panic("x与y不能同时为0")}return x + y}package main import "fmt" func main() { caller() } func caller() { defer func() { if p := recover(); p != nil { err := fmt.Errorf("执行出错: %v", p) fmt.Println(err) } }() r1 := add(1, 2) fmt.Println(r1) r2 := add(0, 0) fmt.Println(r2) } func add(x, y int) int { if x == 0 && y == 0 { panic("x与y不能同时为0") } return x + y }package main import "fmt" func main() { caller() } func caller() { defer func() { if p := recover(); p != nil { err := fmt.Errorf("执行出错: %v", p) fmt.Println(err) } }() r1 := add(1, 2) fmt.Println(r1) r2 := add(0, 0) fmt.Println(r2) } func add(x, y int) int { if x == 0 && y == 0 { panic("x与y不能同时为0") } return x + y }
小结
Go语言的函数是比较重要的内容了,在程序开发过程中,我们总是需要声明自己的函数或者调用其他开发者的函数。
总结一下,这篇文章中,我们讲了以下几点:
- 什么是函数,函数有什么作用
- 函数的声明与使用
- 匿名函数
- defer机制与函数的异常处理