第十二章: Go语言函数

golang-gopher.png

1. 概述

函数是将实现单一或者相关联功能的代码组织起来,内部实现具有封闭性的代码集合,函数可以提高应用程序的模块性和功能代码的复用性.对大多数编程语言而言,函数是很重要的部分.

2. 声明函数

Go语言中函数声明用 func标识这是一个函数 后面跟着 函数名 参数列表 返回参数列表 函数主体内容

// 形式如下
func 函数名称(参数列表) (返回参数列表) {
    函数执行体
}

对函数简单说明 :

  • 函数名的命名和我们声明一个变量是一样的,由字母数字下划线组成.函数名不能以数字开头,同一个包中函数名不能重复

  • 函数名尽量做到见名知意,函数名中字母的大小写不同,会被认为是不同的函数

  • 一个包中的函数名首字母大写,表示该函数能被外部包访问到

    //函数add 和 函数 Add 是两个不同的函数
    func add(){}
    func Add(){}
    
  • 参数列表 : 表示可以存在多个形式参数, 每个形式参数都是由参数变量名参数数据类型 组成

    func demo(x int ,y float64, s string){}
    
  • 返回参数列表 : 可以是返回值的数据类型列表,也可以类似参数列表一样 返回参数变量名数据类型 的组成,函数声明了有返回值,那就必须用 return 语句 提供返回值列表

    func demo1(x int ,y float64)(int,error){
        ...
        ...
        return x+y error 
    }
    
  • 函数执行体 : 能复用的代码片段

2.1 函数基本形式

package main

import (
    "fmt"
    "math"
)
// 基本函数格式写法
func hypot(x float64 ,y float64) (res float64){
    res = math.Sqrt(x*x+y*y)
    // 因为返回值声明了变量res ,所以return 默认将res返回
    return 
}
// 如果参数列表中参数类型相同,可以简写
func swop(x,y int,s1,s2 string) (int,int,string,string){
    x,y = y,x
    s1 ,s2 = s1 ,s2
    // Go语言是支持多返回值得
    // 以为返回值列表中没有声明返回变量,所以return要将返回的值写清楚
    // 定义了多少返回值,就必须将它们都返回,否则编译报错`not enough arguments to return`
    return x,y,s1,s2
}
func main(){
    // 此处都是函数调用
    fmt.Println(hypot(7,9))
    fmt.Println(swop(10,99,"hello","golang"))
}

go run main.go

11.40175425099138
99 10 hello golang

示例:

package main

import "fmt"

const(
    M = 60
    H = M*60
    D = H*24
)
func parseTime(s float64) (m ,h,d float64){
    m = s/M
    h = s/H
    d = s/D
    return
}
func main(){
    s := 1797979
    m,h,d := parseTime(float64(s))
    fmt.Printf("%d秒约等于%f分钟,%f小时,%f天",s,m,h,d)
}

go run main.go

1797979秒约等于29966.316667分钟,499.438611小时,20.809942天

2.2 值传递与引用传递

基本数据类型和数组默认是值传递,在函数内部修改,不影响原有的值

希望函数函数内部修改,能影响到原有的变量,那就采用引用传递,将变量的地址 &传给函数,函数内部以指针 形式操作

:star: 不论是值传递还是引用传递,其实给函数的都是变量副本,不同的是,值传递的是值得拷贝副本,而引用传递则是地址的拷贝,通常地址拷贝因为数据量小,故效率更高.而值拷贝则是数据量越大,效率越差

值传递类型 : 所有int类型 , 所有 float类型 , bool类型 , 数组 , 结构体

引用类型 : 指针 , slice, map , chan , interface 等类型

值传递的示例:

package main

import "fmt"

//都是值传递,拷贝了一个副本数据
func passValue(n int, f float64, s string, arr [3]int) (int, float64, string,[3]int) {
    arr[1] = arr[1] * 100
    return n * 2, f * f, s + "~~~~" ,arr
}
func main() {
    n := 10
    f := 99.9
    s := "hello"
    arr := [3]int{22, 33, 44}
    // 调用函数
    n1, f1, s1 ,arr1:= passValue(n, f, s, arr)
    fmt.Println(n1, f1, s1, arr1)
    // 原来变量的值没有任何影响
    fmt.Println(n, f, s, arr)
}

go run main.go

20 9980.010000000002 hello~~~~ [22 3300 44]
10 99.9 hello [22 33 44]

引用传递示例 :

package main

import "fmt"

// 引用传递1 数据类型本身就时引用类型
func citePass(sli []string, m map[string]string) {
    sli[0] = "AAA"
    m["job"] = "programmer"
}

// 引用传递2 指针形式
func citePass2(n *int) {
    *n = *n +100
}
func main() {
    //定义一个slice
    var sli []string = []string{"aa", "bb", "cc", "dd"}
    //定义一个map
    var person = make(map[string]string)
    person["name"] = "tom"
    person["addr"] = "Provence"
    fmt.Printf("sli的len = %d,cap = %d value = %v\n", len(sli), cap(sli), sli)
    fmt.Println(person)
    //执行函数citePass之后
    citePass(sli, person)
    fmt.Printf("sli的len = %d,cap = %d value = %v\n", len(sli), cap(sli), sli)
    fmt.Println(person)
    n := 10
    citePass2(&n)
    fmt.Println(n)
}

go run main.go

sli的len = 4,cap = 4 value = [aa bb cc dd]
map[addr:Provence name:tom]
sli的len = 4,cap = 4 value = [AAA bb cc dd]
map[addr:Provence job:programmer name:tom]
110

3. 函数变量

在Go语言中,函数像值一样拥有类型,可以赋值给变量,传递给函数,从函数中返回

package main

import (
    "fmt"
    "strings"
)

func demo1(n int) int {
    return n * n
}
func demo2(s string) string{
    return strings.ToUpper(s)
}
func main(){
    // 定义一个函数变量
    f := demo1
    fmt.Printf("f type = %T\n",f)
    fmt.Println(f(9))
    // 声明一个函数类型的变量 s
    var s func(string)string
    s = demo2
    fmt.Printf("s type = %T\n s = %s",s,s("hello golang"))
}

go run main.go

f type = func(int) int
81
s type = func(string) string
 s = HELLO GOLANG

函数作为一个数据类型在其他函数中作为参数的数据类型

package main

import "fmt"

func getSum(x, y int) int {
    return x + y
}
// demo1 的参数类型是func()类型
func demo1(f func(int,int)int, a int,b int) int {
    res := f(a,b)
    return a+res

}
func main() {
    // 定义一个函数类型
    f := getSum
    // 函数类型作为实参传递
    res := demo1(f,20,80)
    fmt.Println(res)
}

go run main.go

120

函数作为返回类型

package main

import "fmt"

func getSum(x, y int) int {
    return x + y
}
// 函数作为返回类型
func demo2() (f func(int,int)int){
    return  getSum
}
func main() {
    rf := demo2()
    fmt.Printf("rf type = %T\n",rf)
    fmt.Println(rf(20,100))
}

go run main.go

rf type = func(int, int) int
120

4. 匿名函数

匿名函数就是没有函数名的函数,只有函数体,匿名函数具有函数的一般特性.可以被传递,赋值给变量等

4.1 定义形式1

定义匿名函数时直接使用

package main

import "fmt"

func main() {
    res := func(x,y int) int {
        return x+y
    }(90,10)
    fmt.Println(res)
}

4.2 定义形式2

将匿名函数赋值给一个变量

package main

import "fmt"

func main() {
    sum := func(x,y int) int {
        return x+y
    }
    fmt.Printf("sum type is %T\n",sum)
    fmt.Println(sum(90,90))
}

4.3 定义形式3

定义成全局匿名函数

package main

import "fmt"

var func1 = func(x,y int) int {
    max :=0
    if x>y{
        max = x
    }else if x<y{
        max = y
    }
    return max
}
func main() {
    fmt.Printf("func1 type is %T\n",func1)
    fmt.Println(func1(90,190))
}

4.4 匿名函数作为回调函数

package main

import "fmt"

func demo(s []int,f func(int)int) []int{
    for k,v:=range s{
         s[k] = f(v)
    }
    return s
}
func main() {
    res := demo([]int{1,2,3,4,5}, func(i int) int {
        return i*i
    })
    fmt.Println(res)
}

4.5 匿名函数示例

package main

import (
    "flag"
    "fmt"
)
var skillString = flag.String("skill","","programmer have skill")
func main(){
    flag.Parse()
    var skill = map[string]func(){
        "sing": func() {
            fmt.Println("会唱歌")
        },
        "dance":func(){
            fmt.Println("会跳舞")
        },
        "rap": func() {
            fmt.Println("会rap")
        },
        "code": func() {
            fmt.Println("会写code")
        },
    }
    if f,ok := skill[*skillString];ok{
        f()
    }else{
        fmt.Println("这个真没有...")
    }
}

5. 闭包

闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使己经离开了
自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。因此,简
单的说 :
函数+引用环境=闭包

package main

import "fmt"

func counter(n int) func() int{
    // 返回一个匿名函数,该匿名函数内引用了函数外的变量n,因此匿名函数和n构成了一个闭包
    return func() int {
        n++
        return n
    }
}
func main(){
    f := counter(1)
    fmt.Printf("%p\n",f)
    fmt.Println(f())
    fmt.Println(f())
    fmt.Println(f())
    fmt.Printf("%p\n",f)
    f1 := counter(10)
    fmt.Printf("%p\n",f1)
    fmt.Println(f1())
    fmt.Println(f1())
    fmt.Println(f1())
    fmt.Printf("%p\n",f1)

}

go run main.go

0x4932a0
2
3
4
0x4932a0
0x4932a0
11
12
13
0x4932a0

另外一个示例

package main

import (
    "fmt"
    "strings"
)

func demo(s string) func(string) string {
    return func(n string) string {
        if !strings.HasSuffix(n, s) {
            return n + s
        }
        return n
    }
}
func main() {
    d := demo(".png")
    fmt.Println(d("aaa"))
    fmt.Println(d("bbb.png"))
}

go run main.go

aaa.png
bbb.png

6. 函数的可变参数

函数的参数列表没有固定的个数的参数

定义格式如下 :

func 函数名(固定参数列表,V ...T)(返回参数列表){
    函数执行体
}
  • V表示可变参数变量,T表示参数的数据类型, 那么V的类型是 []T,数据类型为T的切片
  • ... 固定语法表示参数个数不确定
  • 可变参数一般放在参数列表最尾部

示例 1:

package main

import (
   "fmt"
)

func demo1(s string, sliString ...string) {
   // 可变参数是数据类型为string的切片
   fmt.Printf("%T\n",sliString)
   for _,v:= range  sliString{
       fmt.Println(s+" "+v)
   }
}
func demo2(s ...string) string{
   res := ""
   for _,v:= range s{
       res += v
   }
   return res
}
func main(){
   demo1("hello","ok","nice","yes","good")
   fmt.Println(demo2("Let ","Us ","Go"))
}

go run main.go

[]string
hello ok
hello nice
hello yes
hello good
Let Us Go

示例2: 可变参数函数之间参数传递

package main

import "fmt"

func pri (s ...interface{}){
    for _,v := range s{
        fmt.Println(v)
    }
}
func dump(s ...interface{}){
    pri(s...)
}
func main(){
    dump("golang",2.0)
}

示例3: (获取数据类型)

package main

import (
    "bytes"
    "fmt"
)

func getType(s ...interface{}) string {
    var b bytes.Buffer
    for _, v := range s {
        str := fmt.Sprintf("%v", v)
        var typeS string
        switch v.(type) {
        case bool:
            typeS = "bool"
        case int:
            typeS = "int"
        case string:
            typeS = "string"
        case float64:
            typeS = "float64"
        case []string:
            typeS = "slice"
        default:
            typeS = "unknow"
        }
        b.WriteString("value : ")
        b.WriteString(str)
        b.WriteString(" , type : ")
        b.WriteString(typeS)
        b.WriteString("\n")
    }
    return b.String()
}
func main() {
    res := getType(90, "golang", true, 123.234, []string{"demo"})
    fmt.Println(res)
}

go run mian.go

value : 90 , type : int
value : golang , type : string
value : true , type : bool
value : 123.234 , type : float64
value : [demo] , type : slice

7.延迟调用defer

Go语言中关键字 defer 表示延迟跟在其后面的语句的执行,被延迟的语句按照defer调用的顺序逆向执行,既:最后调用的defer语句,优先执行

  • defer 语句调用遵照先进后出的原则

defer延迟机制,通常讲是为了释放资源,一些常见的创建资源的操作 打开文件,连接数据库,枷锁 等在最后都需要关闭资源句柄,断开连接 释放锁 等操作,这些都可以交给defer来做

7.1 defer的执行顺序

package main

import "fmt"
func demo(s string) {
    fmt.Println(s)
}
func main(){
    fmt.Println("program start")
    defer fmt.Println("one")
    defer demo("two")
    defer func() {
        fmt.Println("three")
    }()
    fmt.Println("program over")
}

go run main.go

program start
program over
three
two
one

7.2 defer 使用示例

示例1 : 文件复制

从A文件复制到B文件,B文件不存在就创建

package main

import (
    "fmt"
    "io"
    "os"
)

func copyFile(dstName, srcName string) (written int64, err error) {
    // open打开一个文件用于读取,读取成功,返回文件对象
    src, err := os.Open(srcName)
    // open错误就直接返回错误
    if err != nil {
        return
    }
    // defer操作关闭文件
    defer src.Close()
    //openFile也是打开文件的函数,可以使用指定项(os.O_RDONLY 只读 os.O_CREATE 不存在就创建新文件 )和指定模式打开文件
    dst, err := os.OpenFile(dstName, os.O_RDONLY|os.O_CREATE, 0644)
    if err != nil {
        return
    }
    // defer操作关闭文件
    defer dst.Close()
    // io.copy()函数将src的数据拷贝到dst,直到src上到达EOF或者错误
    return io.Copy(dst, src)
}
func main() {
    // 调用自定义函数copyFile
    // Args 保存用户命令行参数 []string 类型
    copyFile(os.Args[1], os.Args[2])
    fmt.Println("赋值完成~~")
}

go run main.go "demo.txt" "english.log"

|_main.go
|_demo.txt
|_english.log

示例2 : 获取文件大小

package main

import (
    "fmt"
    "os"
)

func getFileSize(filename string) int64{
    f,err :=os.Open(filename)
    defer f.Close()
    if err != nil{
        return 0
    }
    info ,err := f.Stat()
    if err != nil{
        return 0
    }
    size := info.Size()
    return size

}
func main(){
    // 获取文件的大小(byte)
    fmt.Println(getFileSize(os.Args[1]))
}

8. 错误处理

错误处理是几乎所有编程语言都必须考虑的,Go语言中的错误就是可能出现错误的地方真就出现了错误,我们需要对这样的错误进行处理,这么看错误也是我我们业务代码的一部分,错误处理也将是开发中必须中的必要环节.正确的处理错误,能保障程序更加健壮.

Go语言中引入错误处理的标准模式 ,error 接口,是Go语言内建的接口类型

Go语言中错误处理有如下的特点

  • 可能造成错误的函数,在返回值列表中应该返回一个错误接口 error 如果函数调用成功,错误接口返回的是nil
  • 函数在调用之后需要检查错误,如果有错误,需要进行错误处理
package main

import (
    "fmt"
    "os"
)

func getFileSize(name string) (size int64, err error) {
    size = 0
    f, err := os.Open(name)
    if err != nil {
        return size, err
    }
    info, err := f.Stat()
    if err != nil {
        return size, err
    }
    size = info.Size()
    return size, nil
}
func main() {
    // aa.log 文件不存在
    s, e := getFileSize("aa.log")
    if e != nil {
        fmt.Println(e)
    } else {
        fmt.Println(s)
    }
    fmt.Println("go on ...")
}

go run main.go

open aa.log: The system cannot find the file specified.
go on ...

8.1 自定义错误

Go语言中支持自定义错误,使用 errors.Newpanic 内置函数

  • errors.New("错误说明") 返回一个error类型的值,表示一个错误
  • panic() 内置函数 可以接收任意类型的值,作为参数,输出错误信息,且退出程序
package main

import (
    "errors"
    "fmt"
)

func readConfig(name string) (err error) {
    if name == "config.ini" {
        return nil
    } else {
        return errors.New("读取配置文件错误..")
    }
}
func test() {
    err := readConfig("config2.ini")
    if err != nil {
        // 输出错误信息,并退出程序
        panic(err)
    }
    fmt.Println("test() go on ..")
}
func main() {
    test()
    fmt.Println("client process go on ...")
}

go run main.go

panic: 读取配置文件错误..

goroutine 1 [running]:
main.test()
    /Go/src/GoNote/chapter6/panic-operate/main.go:18 +0x79
main.main()
    /Go/src/GoNote/chapter6/panic-operate/main.go:23 +0x29

8.2 异常的处理

Go语言程序有些错误只能在运行时检查,像数组访问越界,空指针引用等,这些在运行会引起panic异常,一旦发生panic异常,程序就会中断执行,并输出日志,日志包括panic value和函数调用的堆栈跟踪信息

Go语言中处理异常的关键函数是 defer panic recover

简单描述是 : Go语言在运行的时候抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常的处理掉

8.2.1 触发panic

panic 触发一般有三种

  1. 手动触发
  2. 主动触发
  3. 延迟触发

示例 : 手动触发panic

package main

import "fmt"

func main(){
    fmt.Println("program start ...")
    panic("手动触发panic")
    fmt.Println("program end ...")
}

go run main.go

panic: 手动触发panic

goroutine 1 [running]:
main.main()
    E:/Go/src/GoNote/chapter6/demo2/main/main.go:7 +0x9d

示例 : 自动触发panic

package main

import "fmt"

func main() {
    defer fmt.Println("")
    var arr = [...]string{"zero", "one", "two", "three", "four"}
    length := len(arr)
    for i := 0; i <= length; i++ {
        fmt.Println(arr[i])
    }
}

go run main.go

zero
one
two
three
four

panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
    E:/Go/src/GoNote/chapter6/demo3/main/main.go:10 +0x177

示例 : panic 在defer之后执行

package main

import (
    "fmt"
    "time"
)

func demo1() {
    defer func() {
        fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
    }()
}

func main() {
    demo1()
    defer fmt.Println("first ...")
    defer fmt.Println("second ...")
    panic("really over")
}

go run main.go

2019-08-24 16:25:06
second ...
first ...
panic: really over

goroutine 1 [running]:
main.main()
    E:/Go/src/GoNote/chapter6/demo4/main/main.go:18 +0xfd
8.2.2 recover 捕获异常

无论是运行时抛出的panic 还是其他形式的panic,我们都可以配合defer和recover实现异常的捕获和恢复,让程序继续运行下去

本质上Go语言中没有异常系统,使用panic和recover是模拟了其他编程语言中的应对异常的机制

package main

import (
    "fmt"
    "runtime"
)

func divZero(){
    num1 := 10
    num2 := 0
    res := num1 / num2
    fmt.Println("res=", res)
}
func pointCite(){
    var s *string
    *s = "hello world"
}
func Run(f func()){
    defer func() {
        err := recover()
        switch err.(type) {
        case runtime.Error:
            fmt.Println("runtime error : ",err)
        default:
            fmt.Println(err)
        }
    }()
    f()
}
func main(){
    fmt.Println("start===>")
    Run(divZero)
    fmt.Println("go on===>")
    Run(pointCite)
    fmt.Println("end===>")

}

go run main.go

start===>
runtime error :  runtime error: integer divide by zero
go on===>
runtime error :  runtime error: invalid memory address or nil pointer dereference
end===>

panic 和 recover 的关系

  • 有panic 没有recover 程序直接中断
  • 有panic 和recover捕获,程序不会中断,从报panic的点退出后,程序继续执行后面的流程
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容