<Go>3 函数、包和错误处理

本文最后更新于:2023年6月27日 上午

3 函数、包和错误处理

3.1 包

go 以包的形式管理文件和项目目录结构。Go 中的每个文件都属于一个包。

#包的作用:

  • 区分相同名字的函数、变量等标识符
  • 程序文件繁多时,可以很好地管理项目
  • 控制函数、变量等访问范围(作用域)

#语法:

package 包名				// 文件打包
import "包的路径"			// 引入包

#使用细节:

  • 文件打包时,让该包对应文件夹名。包名与文件夹名一致,且为小写字母

  • 当文件要使用其他包的函数或变量时,要先引入对应的包

    一次性引入多个包时,也能这样做

    import (
    	"包名1"
        "包名2"
    )
  • package 指令必须存在于文件第一行,之后才是 import 命令

  • import 包时,编译器自动从 $GOPATH 的 src 目录下开始引入。

  • 访问其他包的函数、变量时,语法是:包名.标识符

  • 如果包名较长,也能为其取别名。但取别名后,原本的包名就不再使用了

    import 别名 "包名"
  • 在同一包下,不能有相同的函数名。否则会导致重复定义

  • main 包是唯一的。如果要编译成一个可执行文件,就需要将该包声明为 main。

    *****>go build [-o bin/my.exe] ****/****/****/main

    [ ] 中是可选内容。加入的场合,在那个指定位置生成指定名称的可执行文件。否则在 $GOPATH 目录下生成默认名称的可执行文件。

3.2 函数

为完成某一功能的程序指令的集合,称为函数。程序不能没有函数,就像西方不能失去耶路撒冷。

在 Go 中,函数分为:自定义函数、系统函数

#基本语法:

func 函数名 (形参列表) (返回值列表) {
    执行语句
    return 返回值列表
}

可以没有返回值。这个场合,也能没有 return 语句

只有一个返回值(且未命名)时,那个返回值列表的括号也能省略

#注意事项:

  • 接收函数返回值时,如果希望忽略某个返回值,可以使用 _ 接收那个希望忽略的值

  • 参数列表和返回值列表的数据类型可以是值类型,也能是引用类型

  • 函数命名遵循标识符命名规范。

  • 函数中的变量是局部的,函数外不生效

  • 基本数据类型与数组的传参方法默认是值传递。在函数内修改不会影响原来的值

    可以传入变量的地址,并在函数内以指针形式操作变量。这样从效果上看类似引用传递

    不管值传递还是引用传递,传递给函数的都是变量的副本。但值传递传递值的拷贝,引用传递传递那个地址的拷贝。

  • Go 函数不支持重载

  • Go 中的函数也是一种数据类型。

    可以把函数赋值给一个变量。这样,该变量就是一个函数类型的变量了。

    Go 中函数是数据类型,因此也能作为形参,并调用

    func main() {
    	a := act
    	fmt.Printf("%T\n", a)			// func(int, int) int
    	fmt.Println(met(act))			// 6
    }
    
    func act(a, b int) int {
    	return a + b
    }
    
    func met(f func(int, int) int) int {
    	return f(1, 5)
    }
  • 为简化数据类型定义,Go 支持自定义数据类型。语法如下:

    type 自定义数据类型 数据类型

    可以理解为给数据类型起了一个别名

    type myfunc func(int, int) int		// 此时 myfunc 等价于 func(int, int) int
    var a myfunc = act
    fmt.Printf("%T\n", a)				// main.myfunc

    虽然起了别名,但 Go 认为是两个不同类型

    type myInt int
    var i int = 1000
    var n myInt = 10
    fmt.Printf("%T\n", i)
    fmt.Printf("%T\n", n)
    /* n = i */					// 无法这样赋值,因为 Go 认为 myInt 不是 int
    n = myInt(i)				// 需要进行类型转换
  • 支持对函数返回值命名

    func act(a int, b int) (int, sum int, sub int) {
    	sum = a + b
    	sub = a - b
        if (sum > 20) {
            return
        } else {
    		return 1, 1, 1
        }
    }
    
    func main() {
    	fmt.Println(act(10, 1))				// 1 1 1
    	fmt.Println(act(100, 1))			// 0 101 99
    }

    对返回值命名时,那些命名的返回值后方的所有返回值也要一并命名。

    只要有对返回值的命名,就能使用单独的 return 语句。届时将所有命名的返回值返回,未命名的返回值返回那个类型的零值

    即使对返回值进行了命名,也能指定返回值。指定返回值时必须指定全部返回值

  • Go 支持可变参数

    func met(args ...int) (sum int) {
    	for _, value := range args {
    		sum += value
    	}
    	return sum
    }

    args 本质是一个 slice 切片。通过 args[index] 访问各个值

  • 函数内部声明 / 定义的变量称为 局部变量。其作用域仅限于函数内部。如果变量在代码块中,其作用域即限于该代码块。

    函数外部声明 / 定义的变量称为 全局变量。作用域在整个包都有效。那个首字母大写的场合,其作用域在整个程序中有效。

3.2.1 init 函数

每个源文件都能包含一个 init 函数。该函数会在 main 函数执行前,被 Go 调用。

通常可以在 init 函数中完成初始化工作。

func init() {
    ...
}

#注意事项:

  • 如果一个文件包含 全局变量定义、init 函数 和 main 函数,其执行流程是:

    全局变量定义 -> init 函数 -> main 函数

  • init 函数最主要的作用是进行初始化工作

  • 引入文件也有 init 函数时,执行流程如下:

    被引入的全局变量定义 -> 被引入的 init 函数 -> 全局变量定义 -> init 函数 -> main 函数

3.2.2 匿名函数

Go 支持匿名函数。如果我们希望某个函数只使用一次,可以使用匿名函数。

匿名函数也能多次调用。

#使用方式:

  • 将匿名函数赋给变量。通过变量来调用

    f := func(n1 int, n2 int) int { 
        return n1 + n2 
    }
    i := f(4, 100)
  • 定义匿名函数时直接调用

    i := func(n1 int, n2 int) int {
        return n1 + n2
    }(4, 7)

    这个调用可以理解成 i := f(4, 100) 中的 f 标识符直接替换为函数对象。

    因为没有命名,所以只能调用一次

  • 将匿名函数赋给全局变量,则其成为全局匿名函数

3.2.3 闭包

闭包就是一个函数和其相关的引用环境组成的整体(实体)

func act() func(int) int {
	i := 0
	return func(a int) (ret int) {
		ret = i
		i += a
		return
	}
}

上述函数返回一个匿名函数,该匿名函数用到函数外的 i。

该匿名函数与 i 是一个整体,即构成一个闭包。

反复调用闭包对象时,i 只初始化一次,故而每次调用都会造成累计:

f := act()
fmt.Println(f(100))				// 0
fmt.Println(f(100))				// 100
fmt.Println(f(100))				// 200
fmt.Println(act()(100))			// 0
fmt.Println(act()(100))			// 0
fmt.Println(act()(100))			// 0

3.2.4 defer 关键字

在函数中,程序员经常需要创建资源(如:数据库连接、文件句柄、锁等),为在函数执行完毕后,及时释放资源,Go 的设计者提供 defer(延时机制)

f := func(a int) {
    defer fmt.Println(a, "(defer1)")
    a += 10
    defer fmt.Println(a, "(defer2)")
    a *= 10
    fmt.Println(a)
}
f(0)

此时,系统依次输出 100 、10(defer2)、0(defer1)

#注意事项:

  • 执行函数时,如果遇到 defer 语句,系统不会执行该语句,而是会将其压入栈中。

    函数执行结束后,再从该栈中依次取出语句(先入后出)并执行。

  • 让 defer 语句入栈时,也会将相关数值一并拷贝入栈

  • defer 最主要的价值是,函数执行完毕后,可以及时释放资源

3.3 常用函数

3.3.1 字符串函数

  • len(v) int:返回传入的 v 的长度。

    传入字符串时,返回那个字节数量。传入数组时,返回那个元素数量

    传入数组指针时,返回 *v 元素数量。传入切片、映射时,返回元素数量

    传入通道时,返回通道缓存中队列未读取的元素数量

  • r := []rune(string):将字符串转为 rune 切片

    这样,能解决中文字符乱码问题

    b := []byte(string):将字符转为 byte 切片

    string([]rune) string:将 rune 切片转为字符串

    string([]byte) string:将 byte 切片转为字符串

以下函数被包含在 strconv 包内:

  • strconv.Atoi(string)(int, error):将字符串转成整数

    strconv.Itoa(int) string:将字符串转成整数

  • strconv.FormatInt(int, int) string:将指定 int 转为指定进制的字符串

    第二个参数可以是 2、8、16

以下函数被包含在 strings 包内:

  • strings.Contains(string, string) bool:查找子串是否在指定字符串中

  • strings.Count(string, string) int:统计字符串中有多少指定的子串

  • strings.EqualFold(string, string) bool:不区分大小写地比较字符串是否相等

  • strings.Index(string, string) int:返回子串第一次出现的 index 值。没有则返回 -1

    strings.LastIndex(string, string) int:返回子串最后一次出现的 index 值

  • strings.Replace(string, string, string, int) string:将指定子串替换为另一个子串

    可以指定要替换的数量。-1 的场合全部替换

    所有的替换的场合,原字符串不变,返回那个替换后的新字符串

  • strings.ToLower(string) string:转为全部小写英文字母

    strings.ToUpper(string) string:转为全部大写英文字母

  • strings.Split(string, string) string[]:按照指定字符对字符串进行分割

  • strings.TrimSpace(string) string:去掉字符串前后的空格

    strings.Trim(string, string) string:去掉字符串前后的所有的指定字符

    那些指定字符指的是第二个字符串参数中,包含的任意字符

    fmt.Print(strings.Trim("1321oAo12", "123"))			// 输出 oAo

    strings.TrimLeft(string, string) string:仅去掉字符串左边的指定字符

    strings.TrimRight(string, string) string

  • strings.HasPrefix(string, string) bool:字符串是否以某子串开头

    strings.HasSuffix(string, string) bool:字符串是否以某子串结尾

3.3.2 时间和日期函数

以下函数被包含在 time 包内:

  • time.Time:Time 类型。代表时间

    time.Now() Time:获取一个当前时间的 Time 对象

  • 获取 Time 对象的某项时间参数

    t := time.Now()

    t.Day() int:获取日数

    t.Month() time.Month:获取月份。利用类型转换才能得到 int 类型的月份

    t.Year() int:获取年份

    t.Hour() intt.Minute() intt.Second() int:获取时、分、秒

    t.Format(string) string:按照字符串格式将格式化该时间。那个格式字符串各项可以自由组合,但时间是固定的,不能变更。即必须是:2006 年 01 月 02 日 15 时 04 分 05 秒

    s := t.Format("2006/01/02 15:04:05")		// 格式字符串必须是该时间
    s2 := t.Format("2006-01-02")
    s3 := t.Format("15:04:05")

    这个时间据说是开发者夹带私货。会开发的就是任性哦~

    t.Unix() int:获取指定时间戳

    t.UnixNano() int64:获取指定纳秒时间戳

  • time.Hour:时间常量,代表 1 小时。

    要获取 10 小时可以使用 10 * time.Hour。可以使用加法、减法。但不能使用除法

    time.Minutetime.Second:分钟、秒

    time.Millisecond:毫秒。1000 毫秒 == 1 秒

    time.Microsecond:微秒。1000 微秒 == 1 毫秒

    time.Nanosecond:纳秒。1000 纳秒 == 1 微秒

    以上这些时间常量的类型是 time.Duration

  • time.sleep(time.Duration):休眠指定时间

3.4 内置函数

Go 的设计者为了编程方便,提供了一些函数。这些函数能直接使用。即为内置函数。

  • len(v) int:返回传入的 v 的长度。取决于 v 的类型

    cap(v) int:返回 v 的容量。取决于 v 的类型

  • new(Type) *Type:使用 new 来分配内存。返回值为一个指向该类型新分配的零值的指针

  • make(Type, IntegerType) Type:分配并初始化一个类型为切片、映射或通道的对象

  • recover() error:捕获一个异常

3.5 错误处理

在默认情况下,程序发生错误(panic)后就会退出(崩溃)

如果希望发生错误后,可以捕获错误并进行处理,使程序得以继续执行,还能进行提示,就需要错误处理

Go 语言追求简洁优雅。Go 通过 defer、panic、recover 进行错误处理:

Go 可以抛出 panic 异常,并在 defer 中通过 recover 捕获该异常,然后进行处理

defer func() {
    err := recover()
    if err != nil {
        fmt.Println("Error -", err)
    }
}()
i := 10
n := 0
fmt.Println(i / n)				// 这里出现一处除以 0 的错误

以上代码输出:Error -runtime error: integer divide by zero

加入错误处理后,程序不会轻易崩溃。如果加入预警代码,就能增强代码健壮性

#自定义错误:

Go 语言支持自定义错误。

  • errors.New(string) error:传入错误说明,返回一个 error 对象
  • panic(interface{}):输出错误信息,并退出程序
defer func() {
    fmt.Println(recover(), "!!!")
}()
panic(errors.New("???"))

输出:??? !!!


<Go>3 函数、包和错误处理
https://i-melody.github.io/2022/06/08/Go/3 函数、包和错误处理/
作者
Melody
发布于
2022年6月8日
许可协议