Golang教程第5篇-函数

1.Golang函数特点

  • 不支持重载,一个包不能有两个名字一样的函数
  • 可以将函数赋值给变量
  • 支持匿名
  • 支持多返回值

2.函数赋值和函数传递函数

下面是一个将函数赋值给一个变量的例子

package main 

import (
    "fmt"
)
 
func add(a,b int) int {
    return a + b
}

func main() {
    c := add
    fmt.Printf("%v %T\n",c,c)
    sum := c(100,200)
    fmt.Printf("%d\n",sum)
}

编译执行
05go30

代码中将函数add赋值给变量c,c存储的是一串地址,其类型为func(int,int) int,通过c来调用函数打印函数的结果

既然函数可以赋值给变量,那么函数也是可以作为参数进行传递的

package main

import (
    "fmt"
)

type addFun func(int,int) int  

func Add(a int,b int) int {
    return a + b
}

func  Operate(op addFun,a int,b int) int {
    return op(a,b)
}

func main() {
    c := Add
    fmt.Printf("%v %T\n",c,c)
    
    sum := Operate(c,100,400)
    fmt.Printf("%d\n",c)
}

编译执行
05go29
通过type关键字定义了一个类,名字叫addFun,该类的类型为func(int,int) int,怎么理解呢?因为函数也是可以通过变量在函数中进行传递的,比如上面的Operate函数就传递了3个参数,其中op这个参数就是一个函数,类似于传递int变量,函数传递也需要表示其类型,而这里的类型就是自定义addFun,且addFun要是func(int,int) int类型的,与自定义的Add函数一致,否则会报错

上面代码中通过Operate函数传递函数参数时可以直接使用传递的函数Operate(Add,100,400),定义函数Operate时也可以直接使用func Operate(op func(int,int) int,a,b) int,给传递的函数变量的类型func(int,int) int自定义一个名字更为方便,比如有的函数有很多个返回值的情况

函数变量c存的是函数地址

3.函数传递

分为值传递和引用传递,基本数据类型都是值传递,map、chan、slice、指针、interface都是引用传递,但是不管是哪种传递,都是通过拷贝一个副本导函数中去进行计算

4.函数返回值命名

先看一个常见的加法例子

package main

import (
    "fmt"
)

func Calc(a,b int) (int,int) {
    return a + b,(a + b)/2
}

func main() {
    var a int = 100
    var b int = 200
    sum,avg := Calc(a,b)
    fmt.Printf("sum=%d\n",sum)
    fmt.Printf("avg=%d\n",avg)
}

上面的程序中Calc函数返回了两个整型,其实可以给这两个返回值命名,将函数Calc修改成下面这样

func Calc(a,b int) (sun int,avg int) {
    sum = a + b
    avg = (a + b)/2
    return
}

命名的好处是可以不用管返回值列表中的顺序

5.可变参数

参数传递有时候会传入可变参数,类似如下结构

func Add(arg...int) int {} 传入0个或多个参数   
func Add(a int,arg...int) int {} 传入1个或多个参数
func Add(a string,b string,arg...string) string {}传入2个或多个参数   

上面的arg类型为slice,可以通过arg[index]来依次访问各个参数

下面通过两个例子来具体说明可变参数的用法
例子1:写一个函数,支持一个或多个int相加,并返回相加

package main

import (
    "fmt
)

func Add(a int,arg...int) int {
    var result int = a
    intlen := len(arg)
    for i := 0;i < intlen;i++{
        result +=  arg[i]
    }
    return result
}

func main() {
    var a int = 10
    sum := Add(a,10,20,30)
    fmt.Println(sum)
}

例子2:写一个函数,支持1个或多个string相拼接,并返回结果

package main

func Add(str string,arg...string) string {
    var result string = str
    strlen := len(arg)
    for i := 0;i < strlen;i++ {
        result = result + " " + arg[i]
    }
    return result
}

func main() {
    var str string = "hello"
    strall := Add(str,"world","中国")
    fmt.Println(sum)
}

6.defer

  • 函数返回时执行defer语句,可以资源清理
  • 多个defer语句按照先进后出的原则,代码块中先定义的后执行
  • defer语句中的变量是在声明defer语句的时候就确定了,不会因为变量后面的变动而改变

针对后面这俩个点可以看下面这个例子

package main

import (
    "fmt"
)

func main() {
    var i int = 0
    defer fmt.Println(i)
    defer fmt.Println("seconds")
    
    i = 10
    fmt.Println(i)
}   

编译执行

05go32

再来个例子

package main

import (
    "fmt"
)

func main() {
    for i := 0;i < 10;i ++ {
        defer fmt.Println(i)
    }
}

编译执行
05go33

defer语句的用途
以下均为伪代码

用途1:关闭文件句柄

func Read() {
    file := open(filename)
    defer file.close()
}

用途2:锁资源释放

func Read() {
    mc.Lock()
    defer mc.Unlock()
}

注意只是释放当前锁,这里时mc

用途3:数据库连接释放

func Read() {
    conn := openDatabase()
    defer conn.close()
}

7.匿名函数调用

函数也支持匿名

package main

import (
    "fmt"
)

func Test(a int,b int) int {
    c := func(a int,b int) int {
        return a + b
    }(100,200)
    return c
}

func main() {
    result := c(100,200)
    fmt.Println(result)
}

上面是在定义匿名函数时直接调用,当然也可以不直接调用

package main

import (
    "fmt"
)

func Test(a,b int) int {
    c := func(a,b int) int {
        return a + b
    }
    return c(100,200)
}

func main() {
    result := Test(100,200)
    fmt.Println(result)
}

8.rune使用

在Go里,中文字符占3个byte,当我们去计算一个带有中文字符的字符串长度的时候是按照字符串字节长度来计算的,比如下面

package main

import (
    "fmt"
)

func main() {
    var str string = "hello world 中国"
    fmt.Printf("%d\n",len(str))
    var result []rune = []rune(str)
    fmt.Printf("%d\n",len(result))
    
    fmt.Println()
    
    fmt.Println("str遍历")
    for i,v := range str {
        fmt.Printf("%d %c %d\n",i,v,len(string(v)))
    }
    
    fmt.Println()
    
    fmt.Println("rune遍历")
    for i,v := range result {
        fmt.Printf("%d %c %d\n",i,v,len(string(v)))
    }
}

编译执行
05go42

从上面的图中可以看到遍历str字符串的长度为18,遍历rune的长度是14,注意图中中文字符的索引值
通过比较发现str计算的是实际字符串占用的字节数,但是rune计算的是一个字符串占用的位置的长度,这就表示如果字符串中有中文字符,最好将字符串转换成[]rune类型,然后通过[]rune的索引才能正确分离需要的中文字符

代码中计算遍历出来的字符长度时需要进行字符串转换,否则无法计算,go里只有字符串、切片(比如[]string、[]byte、[]rune等)、map等可以进行长度计算,单个的byte、rune是无法进行长度计算的

9.练习

练习1:一个数如果恰好等于它的因子之和,这个数称为"完数",例如6=3+2+1,编程找出1000以内的所有完数

package main

import (
    "fmt"
)

func Tongji(a int) bool {
    var flag bool
    var result int
    
    for j := 1;j < a;j++ {
        if a%j == 0 {
            result += j
        }
    }
    if result == a {
        flag = true
    }
    
    return flag
}

func main() {
    var a int
    fmt.Scanf("%d\n",&a)
    
    for i := 1;i < a;i++ {
        result := Tongji(i)
        if result == true {
            fmt.Printf("%d is a 完数\n",i)
        } 
    }
}

编译执行
05go34

比较一下老师写的代码

package main

import (
    "fmt"
)

func Perfect(n int) bool {
    var flag bool
    var sum int
    for i := 1;i < n;i++ {
        if n%i == 0 {
            sum = sum + i
        }
    }
    if sum == n {
        flag = true
    }
    return flag
}

func Process(n int) {
    for i := 1;i < n + 1;i++ {
        if Perfect(i)
           fmt.Println(i)
        } 
    }
}

func main() {
    var n int
    fmt.Scanf("%d",&n) 
    Process(n)
}

这种写法分别把计算完数过程、输出分别用两个函数实现,整个函数的输入从入口函数输入,这种写法更具有可读性

练习2:输入一个字符串,判断其是否为回文,回文是指从左到右和从右到左读完全相同的字符串

package main

import (
    "fmt"
)

func Huiwen(str string) bool {
    var result []byte
    var flag bool
    tmp := []byte(str)
    strlen := len(str)
    for i := 0;i < strlen;i++ {
        result = append(result,tmp[strlen-i-1])
    }
    if string(result) == str {
        flag = true
    }
    
    return ture
}

func main() {
    var str string
    fmt.Scanf("%s\n",&str)
    
    result := Huiwen(str)
    if result == true {
        fmt.Printf("%s is a 回文\n",str)
    } else {
        fmt.Printf("%s is not a 回文\n",str)
    }
}

05go36
单上面这种写法只能针对全英文字符的字符串,一个字符占一个字节,如果字符串里有中文,上面代码就会报错,看下面的输出例子
05go38

package main

import (
    "fmt"
)

func Huiwen(str string) bool {
    var flag bool = true
    var result []rune 
    result = []rune(str)
    
    for i,_ := range result {
        last := len(result) - i -1
        if result[i] != result[last] {
            flag = false
        }
    }
    return flag
}

func main() {
    var str string
    fmt.Scanf("%s",&str)
    result := Huiwen(str)
    if result {
        fmt.Printf("%s is a 回文\n",str)
    } else {
        fmt.Printf("%s is not a 回文\n",str)
    }
}

编译执行
05go39
针对输入的字符串中有中文字符的情况,使用rune类型来进行遍历,其实针对回文字符串判断不需要将整个字符串全遍历,只需要遍历一半即可,看下面的写法

func Huiwen(str string) bool {
    var flag bool = true
    result := []rune(str)
    for i := 0;i < len(result);i++ {
        if i == len(result)/2 {
            break
        }
        if result[i] != result[len(result)-i-1] {
            flag = false
        }
    }
    return flag
}

上面有代码中当遍历到i等于一半的时候就跳出遍历,同时对值的判定必须使用!=来进行失败判定,因为只要遍历的时候一次失败就证明肯定不是回文,但==进行成功判定并不能保证所有的遍历会成功或者失败,flag的值就不确定了,这就无法进行回文判定

对于纯英文字符上面在实现字符串拼接的时候使用了内置函数append,该函数传递的变量为[]byte,所以才需要定义一个类型为[]byte的result变量,而字符串通过+拼接是可以直接把字符串与字符进行拼接的,下面换成+来实现字符串拼接的功能,这里只写自定义函数部分

func Huiwen(str string) bool {
    var result string
    strlen := len(str)
    var flag bool
    for i := 0;i < strlen;i++ {
        result = result + fmt.Sprintf("%c",str[strlen-i-1])
    }
    if result == str {
        flag = true
    }
    return flag
}

练习3:输入一行字符,分别统计出其中英文字母、空格、数字和其他字符的个数

package main

import (
    "fmt"
)

func Tongji(str string) (num1 int,num2 int,num3 int,num4,num5 int) {
    for _,v := range str {
        if Unicode.IsLetter(v) == true && len(string(v)) == 1 {
            num1++
        } else if  Unicode.IsLetter(v) == true && len(string(v)) == 3 {
            num5++
        } else if Unicode.IsSpace(v) == true {
            num2++
        } else if Unicode.IsDigit(v) == true {
            num3++
        } else {
            num4++
        }
    }
    return 
}

func main() {
    var str string
    fmt.Scanf("%s\n",&str)
    
    num1,num2,num3,num4,num5 := Tongji(str)
    fmt.Printf("字母有:%d个\n",num1)
    fmt.Printf("空格有:%d个\n",num2)
    fmt.Printf("数字有:%d个\n",num3)
    fmt.Printf("其他有:%d个\n",num4)
    fmt.Printf("中文字符有:%d个\n",num5)
}

编译执行
05go43

上面使用for range语句取字符串的每一个位置的值,注意这里取的值的类型是rune,而包unicode调用各个函数需要的变量也是rune,刚好配合好,比如下面使用字符的类型来处理就需要转换

func Tongji(str string) (num1 int,num2 int,num3 int,num4 int,num5 int) {
    result := []rune(str)
    length := len(result)
    for i := 0;i < length;i++ {
        if unicode.IsLetter(result[i]) == true && len(string(result[i])) == 1 {
            num1++
        } else if unicode.IsLetter(result[i]) == true && len(string(result[i])) == 3 {
        num5++
        } else if unicode.IsSpace(rune(strlen[i])) == true {
            num2++
        } else if unicode.IsDigit(rune(strlen[i])) == true {
            num3++
        } else {
            num4++
        }
    }
    return 
} 

但是上面这两种写法都只适用于读取一段字符串,但对于在终端读取一段字符就不适用了,比如
05go44
从上面图中可以看到对于终端输入的一段带有空格的字符上面的代码就无法正确输出了,那么该如何修改代码实现正确的输出呢?

老师写的代码

package main

import (
    "fmt"
    "os"
    "bufio"
)

func Count(str string) (wc,nc,sp,zh,oc int) {
    result := []rune(str)
    for i,v := range result {
        switch {
        case v >= 'a' && v <= 'z':
            wc++
        case v >= 'A' && v <= 'Z':
            wc++
        case v == ' ':
            sp++
        case v >= '0' && v <= '9':
             nc++
        case len(string(result[i])) == 3:
             zh++
        default:
              oc++
        }
    }
    return
}

func main() {
    read := bufio.NewReader(os.Stdin)
    result,_,err := read.ReadLine()
    if err != nil {
        fmt.Printf("can not read from console:",err)
        return
    }
    
    wc,nc,sp,zh,oc := Count(string(result))
    fmt.Printf("wordnumber is %d\n",wc)
    fmt.Printf("nuumber count is %d\n",nc)
    fmt.Printf("sp count is %d\n",sp)
    fmt.Printf("zh count is %d\n",zh)
    fmt.Printf("other ccount is %d\n",oc)
}

05go45

练习4:两个大数相加,这两个大数的范围都超过int64

package main

import (
    "fmt"
    "os"
    "strings"
    "bufio"
)

func Multi(str1,str2 string) (result string) {
    if len(str1) == 0 && len(str2) == 0 {   // 无输入情况
        result = string('0')
        return
    }
    
    var left int                 // 进位值
    var sum int                  // 位相加的值
    index1 := len(str1) - 1      // str1最大索引值
    index2 := len(str2) - 1       // str2最大索引值 
    
    // str1与str2等长位字符计算
    for index1 >= 0 && index2 >= 0 {  
        c1 := str1[index1] - '0'  // str1位字符计算
        c2 := str2[index2] - '0'  // str2位字符计算
        c3 := int(c1) + int(c2) + left // c1与c2之和并转成数值型,注意这里有left
        if c3 >= 10 {
            left = 1              
        } else {
            left = 0
        }
        sum = (c3 % 10) + '0'     // 相加之后获得的值,这里是数值型结果
        result = fmt.Sprintf("%c%s",sum,result) // sum以字符输出拼接到result字符串的前面
        index1--   // 自减
        index2--
    }
    
    // str1比str2长的部分字符计算
    for index1 >= 0 {
        c1 := str1[index1] - '0'
        c3 := int(c1) + left
        if c3 >= 10 {
            left = 1
        } else {
            left = 0
        }
        sum = (c3 % 10) + '0'
        result = fmt.Sprintf("%c%s",sum,result)
        index1--
    }
    
    // str2比str1长的部分字符计算
     for index2 >= 0 {
        c2 := str2[index2] - '0'
        c3 := int(c2) + left
        if c3 >= 10 {
            left = 1
        } else {
            left = 0
        }
        sum = (c3 % 10) + '0'
        result = fmt.Sprintf("%c%s",sum,result)
        index2--
    }
    
    if left == 1 {
        result = fmt.Sprintf("1%s",result)
    }
    return
}

func main() {
    read := bufio.NewReader(os.Stdin) // 从终端读取输入
    result,_,err := read.ReadLine()  // 读取一行
    if err != nil {
        fmt.Printf("read from console err")
        return
    }
    slicestr := strings.Split(string(result),"+") // 分隔生成slice
    if len(slicestr) !=2 {
        fmt.Printf("read from console input a + b")
        return
    }
    slice1 := strings.TrimSpace(slicestr[0])  // 获取子串
    slice2 := strings.TrimSpace(slicestr[1])
    Multiresult := Multi(slice1,slice2)
    fmt.Printf("%s",Multiresult)
    
}

编译执行
05go46

字符在Go里默认是以十进制ascii值存储,比如两个字符相加其实是其两个ascii码相加,代码中就是利用这一特性,对数字字符串进行模拟加减,最后每一位上计算获得的ascii码值转换成字符与result进行拼接

为了加深对字符的计算,可以看下下面这个例子及其输出

package main

import (
    "fmt"
)

func main() {
var str string = "abc"
var d int
var c byte = 'c'
sum := str[0] + '0'
b := '9' - '0'
d = 1 - '0'
c = c + '0'
fmt.Printf("%T %v\n",sum,sum)
fmt.Printf("%T %v\n",b,b)
fmt.Printf("%T %v\n",d,d)
fmt.Printf("%T %v\n",c,c)
}

编译执行
05go47

字符之间计算实际是每个字符的的ascii值计算,字符与数字之间计算是数字与字符的ascii值的计算,上面的输出都是输出整型,也可以以%c输出字符型

练习5、两个大数相减
类似两个大数相加,使用字符串模拟

package main

import (
    "fmt"
    "os"
    "strings"
    "bufio"
)

func Multi(str1,str2 string) (result string) {
    if len(str1) == 0 && len(str2) == 0 {  // 无输入返回字符串“0”
        result = string('0')
        return
    }
    
    index1 := len(str1) - 1
    index2 := len(str2) - 1
    var left int
    
    if len(str1) > len(str2) {                      // str1长度大于str2
        for index1 >= 0 && index2 >= 0 {
            c1 := str1[index1] - '0'
            c2 := str2[index2] - '0'
            c3 := int(c1) - int(c2) + left
            var sum int
            if c3 < 0 {
                left = -1
                sum = (c3 + 10) + '0'
            } else {
                left = 0
                sum = c3 + '0'
            }
            result = fmt.Sprintf("%c%s",sum,result)
            index1--
            index2--
        }
        
        for index1 >= 0 {
            c1 := str1[index1] - '0'
            c3 := int(c1) + left
            var sum int
            if c3 < 0 {
                left = -1
                sum = (c3 + 10) + '0'
            } else {
                left = 0
                sum = c3 + '0'
            }
            result = fmt.Sprintf("%c%s",sum,result)
            index1--
        }       
    } else if len(str1) < len(str2) {    // str1长度小于str2
        for index1 >= 0 && index2 >= 0 {
            c1 := str1[index1] - '0'
            c2 := str2[index2] - '0'
            c3 := int(c2) - int(c1) + left
            var sum int
            if c3 < 0 {
                left = -1
                sum = (c3 + 10) + '0'
            } else {
                left = 0
                sum = c3 + '0'
            }
            result =fmt.Sprintf("%c%s",sum,result)
            index2--
            index1--
        }
        
        for index2 >= 0 {
            c2 := str2[index2] - '0'
            c3 := int(c2) + left
            var sum int
            if c3 < 0 {
                left = -1
                sum = (c3 + 10) + '0'
            } else {
                left = 0
                sum = c3 + '0'
            }
            result = fmt.Sprintf("%c%s",sum,result)
            index2--
        }
        result = fmt.Sprintf("-%s",result)   // 因为是小的减去大的有负号
    } else {    // str1长度与str2长度相等
        for index1 >= 0 && index2 >= 0 {   // 获取最后的借位值
           c1 := str1[index1] - '0'
           c2 := str2[index2] - '0'
           c3 := int(c1) - int(c2) + left
          
           if c3 < 0 {
              left = -1
           } else {
              left = 0
           } 
           index1--
           index2--
        }
        
        if left < 0 {   // 借位值小于0表示str1是小于str2的
            var lefts int// 新借位值,注意因为上面在判断left的值时index1 index2已经为0,这里需要重新定义新的索引最大值来进行计算,这里用的是局部变量        
            index3 := len(str1) - 1
            index4 := len(str2) - 1
            for index3 >= 0 && index4 >= 0 {
                c3 := str1[index3] - '0'
                c4 := str2[index4] - '0'
                c5 := int(c4) - int(c3) + lefts
                var sum int         
                if c5 < 0 {
                    lefts = -1
                    sum = (c5 + 10) + '0'
                } else {
                    lefts = 0
                    sum = c5 + '0'
                }
                result = fmt.Sprintf("%c%s",sum,result)
                index3--
                index4--
            }
            result = fmt.Sprintf("-%s",result)        
        } else {  // 借位值等于表示str1是大于等于str2的
            var lefts int
            index3 := len(str1) - 1
            index4 := len(str2) - 1
            for index3 >= 0 && index4 >= 0 {
                c3 := str1[index3] - '0'
                c4 := str2[index4] - '0'
                c5 := int(c3) - int(c4) + lefts
                var sum int
                if c5 < 0 {
                    lefts = -1
                    sum = (c5 + 10) + '0'
                } else {
                    lefts = 0
                    sum = c5 + '0'
                }
                result = fmt.Sprintf("%c%s",sum,result)
                index3--
                index4--
            }
        }
        
     }
     return 
}

func main() {
    read := bufio.NewReader(os.Stdin)
    result,_,err := read.ReadLine()
    if err != nil {
        fmt.Printf("read from console err")
        return
    }
    slicestr := strings.Split(string(result),"-")
    if len(slicestr) != 2 {
        fmt.Printf("please input a -b")
        return 
    }
    slice1 := strings.TrimSpace(slicestr[0])
    slice2 := strings.TrimSpace(slicestr[1])
    fmt.Println(Multi(slice1,slice2))
}

编译执行
05go48
减法与加法还是有区别的,存在着正负之分,所以需要分类讨论