函数详解、time包及日期函数
1、函数
1.1、函数定义
函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了Go语言中函数的相关内容。
Go语言中支持:函数、匿名函数和闭包。
Go语言中定义函数使用 func 关键字,具体格式如下:
定义两个函数sumFn和subFn,分别对两个整数进行求和与差值。
go
package main
import "fmt"
func sumFn(x int, y int) int {
res := x + y
return res
}
func subFn(x int, y int) int {
res := x - y
return res
}
func main() {
res1 := sumFn(10, 2)
res2 := subFn(5, 3)
fmt.Println(res1, res2)
}

1.2、函数参数
类型简写,函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:
go
package main
import "fmt"
func sumFn(x, y int) int {
return x + y
}
func main() {
fmt.Println(sumFn(5, 10))
}

可变参数:可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。
注意: 可变参数通常要作为函数的最后一个参数。
go
package main
import "fmt"
func sumFn(x ...int) {
fmt.Printf("值: %v, 类型: %T\n", x, x)
}
func main() {
sumFn(1, 2, 3, 4, 5)
}
可以看到可变参数x的类型是切片,因此我们可以遍历切片计算出总和。
go
package main
import "fmt"
func sumFn(x ...int) int {
fmt.Printf("值: %v, 类型: %T\n", x, x)
sum := 0
for _, v := range x {
sum += v
}
return sum
}
func main() {
res := sumFn(1, 2, 3, 4, 5)
fmt.Println(res)
}

固定参数搭配可变参数使用如下:
go
package main
import "fmt"
func sumFn(x int, y ...int) int {
fmt.Printf("值: %v, 类型: %T\n", y, y)
sum := x
for _, v := range y {
sum += v
}
return sum
}
func main() {
res := sumFn(100, 1, 2, 3, 4)
fmt.Println(res)
}

1.3、函数返回值
Go语言中通过return关键字向外输出返回值。
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。
例如实现一个clac函数,既计算两数之和,也计算两数之差,返回两个返回值。
go
package main
import "fmt"
func clac(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
func main() {
a, b := clac(10, 2)
fmt.Println(a, b)
}

返回值命名:函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
go
package main
import "fmt"
func clac(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
func main() {
a, b := clac(8, 3)
fmt.Println(a, b)
}

将之前我们写的选择排序和冒泡排序实现成一个函数:
go
package main
import "fmt"
func SelectSort(arr []int) {
for i := 0; i < len(arr); i++ {
for j := i + 1; j < len(arr); j++ {
if arr[i] > arr[j] {
tmp := arr[i]
arr[i] = arr[j]
arr[j] = tmp
}
}
}
}
func BubbleSort(arr []int) {
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr)-i-1; j++ {
if arr[j] < arr[j+1] {
tmp := arr[j]
arr[j] = arr[j+1]
arr[j+1] = tmp
}
}
}
}
func main() {
var sliceA = []int{11, 3, 213, 45, 63, 0, 10}
SelectSort(sliceA)
fmt.Println(sliceA)
BubbleSort(sliceA)
fmt.Println(sliceA)
}

下面实现一个函数将map类型数据按照key进行排序,把key=>value添加到字符串中返回。
go
package main
import (
"fmt"
"sort"
)
func mapSort(map1 map[string]string) string {
var arr []string
for k, _ := range map1 {
arr = append(arr, k)
}
sort.Strings(arr)
var res string
for _, v := range arr {
res += fmt.Sprintf("%v=>%v", v, map1[v])
}
return res
}
func main() {
m1 := map[string]string{
"username": "张三",
"age": "20",
"sex": "男",
"height": "180cm",
}
res := mapSort(m1)
fmt.Println(res)
}

函数变量作用域:
全局变量:全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效 (全局作用域)
局部变量:局部变量是函数内部定义的变量,函数内定义的变量无法在该函数外使用 (局部作用域)
1.4、函数类型与变量
定义函数类型:我们可以使用type关键字来定义一个函数类型,具体格式如下:
上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
go
package main
import "fmt"
type clac func(int, int) int
type myInt int
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func main() {
var c clac
c = add
fmt.Printf("值: %v, 类型: %T\n", c, c)
fmt.Println(c(3, 6))
f := sub
fmt.Printf("值: %v, 类型: %T\n", f, f)
fmt.Println(f(10, 5))
var a int = 10
var b myInt = 20
fmt.Printf("a的类型: %T, b的类型: %T\n", a, b)
fmt.Println(a + int(b))
}
可以看到c的类型为main.clac,而f的类型则是func(int, int) int。另外a和b的类型也是不同的,所以相加时需要进行强转。
1.5、函数作参数和返回值
函数可以作为另一个函数的参数:
go
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
type clacType func(int, int) int
func clac(x, y int, cb clacType) int {
return cb(x, y)
}
func main() {
res1 := clac(10, 5, add)
res2 := clac(10, 5, sub)
res3 := clac(10, 5, func(x, y int) int {
return x * y
})
fmt.Println(res1, res2, res3)
}

对于前两个计算,我们传入了已有的函数add和sub。第三个计算我们传入了一个匿名函数,通俗来说就是没有名字的函数。
函数也可以作为返回值:
go
package main
import "fmt"
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
type clacType func(int, int) int
func do(op string) clacType {
switch op {
case "+":
return add
case "-":
return sub
case "*":
return func(x, y int) int {
return x * y
}
default:
return nil
}
}
func main() {
f1 := do("+")
f2 := do("*")
fmt.Println(f1(3, 4), f2(3, 4))
}

1.6、匿名函数、函数递归和闭包
匿名函数就是没有函数名的函数, 匿名函数的定义格式如下:
匿名函数因为没有函数名, 所以没办法像普通函数那样调用, 所以匿名函数需要保存到某个变量或者作为立即执行函数。
go
package main
import "fmt"
func main() {
// 匿名自执行函数
func() {
fmt.Println("test...")
}()
var fn = func(x, y int) int {
return x * y
}
fmt.Println(fn(3, 4))
// 匿名自执行函数接收参数
func(x, y int) {
fmt.Println(x + y)
}(3, 4)
}

函数内部可以再调用其他函数,也可以调用本函数,调用本函数就是函数递归。
比如,利用递归计算出1 + 2 + 3 + ... + 100
go
package main
import "fmt"
func sum(x int) int {
if x == 1 {
return 1
}
return x + sum(x-1)
}
func main() {
fmt.Println(sum(100))
}

全局变量的特点:常驻内存,污染全局。
局部变量的特点:不常住内存,不污染全局。
这里的污染全局就比如,在全局定义了一个变量a,在某个函数内容又定义了一个变量a,那么在访问的时候就会优先访问局部变量a。
闭包:可以让一个变量常驻内存且不污染全局。
闭包:
1、闭包是指有权访问另一个函数作用域中的变量的函数。
2、创建闭包的常见的方式就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量。
如下:
go
package main
import "fmt"
func adder1() func() int {
var x = 10
return func() int {
return x + 1
}
}
func adder2() func(int) int {
var x = 10
return func(y int) int {
x += y
return x
}
}
func main() {
var fn1 = adder1()
fmt.Println(fn1())
fmt.Println(fn1())
fmt.Println(fn1())
fn2 := adder2()
fmt.Println(fn2(10))
fmt.Println(fn2(10))
fmt.Println(fn2(10))
}
由于闭包里作用域返回的局部变量资源不会被立刻销毁回收,所以可能会占用更多的内存。过度使用闭包会导致性能下降,建议在非常有必要的时候才使用闭包。
1.7、defer语句
Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被defer的语句,最先被执行。
go
package main
import "fmt"
func main() {
fmt.Println("开始")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("结束")
}
上面的中间三条打印语句被延迟执行了,并且执行的时候会逆序执行,有点像栈的结构特点。
go
package main
import "fmt"
func test() {
fmt.Println("开始")
defer func() {
fmt.Println("aaa")
fmt.Println("bbb")
}()
fmt.Println("结束")
}
func main() {
test()
}

defer的执行时机:
defer在命名返回值和匿名返回函数中的表现不同。
go
package main
import "fmt"
func f1() int {
var x = 0
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return x
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
}
f1()的分析:
返回值机制:匿名返回值时,return x 实际上是将x的值(此时为0)复制到一个临时返回值变量中。
defer的执行:defer在return之后执行,修改的是局部变量x(从0→1),但临时返回值变量不受影响。
结果:函数返回临时变量存储的值0。
f2()的分析:
返回值机制:命名返回值时,x既是局部变量又是返回值变量。return x 直接返回x变量本身(此时值为0)。
defer的执行:defer在return之后执行,直接修改返回值变量x(从0→1)。
结果:函数返回被修改后的 x(值为 1)。
下面再来看一个例子,思考一下输出什么?
go
package main
import "fmt"
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x) //defer注册要延迟执行的函数时该函数所有的参数都需要确定其值
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
f1()分析:
1、匿名返回值(未命名)
2、return x 将 x 的当前值 (5) 复制到临时返回值变量
3、defer 增加局部变量 x 的值 (5→6),但不影响临时返回值
4、返回临时变量的值 (5)
f2()分析:
1、命名返回值 x
2、return 5 等同于 x = 5
3、defer 直接修改返回值变量 x (5→6)
4、返回被修改后的x
f3()分析:
1、命名返回值 y
2、return x 等同于 y = x (将局部变量 x 的值 5 复制给 y)
3、defer 修改局部变量 x (5→6),但不影响返回值变量 y
4、返回 y (5)
f4()分析:
1、命名返回值 x
2、defer 接受参数时,参数值在注册时确定:
此时 x 还是初始零值 0(命名返回值初始化为0)
传入的 x 是值拷贝 (0)
3、return 5 将返回值变量 x 设为 5
4、defer 执行时,操作的是参数拷贝(0→1),不影响返回值变量 x
5、返回 x (5)
再来看另一个案例:
go
package main
import "fmt"
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}

1.8、panic和recover

Go 语言中目前是没有异常机制,但是可以使用panic/recover模式来处理错误。
go
package main
import "fmt"
func fn() {
defer func() {
err := recover()
if err != nil {
fmt.Println("err:", err)
}
}()
panic("抛出一个异常")
}
func main() {
fn()
}

实现一个计算两数相除的函数,如果被除数为0,那么就会程序崩溃,这时候我们就可以使用defer + recover来处理,此时程序不会直接崩溃,会继续向后执行。
go
package main
import "fmt"
func div(x, y int) int {
defer func() {
err := recover()
if err != nil {
fmt.Println("err:", err)
}
}()
return x / y
}
func main() {
fmt.Println(div(10, 0))
fmt.Println(div(10, 2))
fmt.Println(div(10, 5))
}

defer、panic、recover的配合使用:
go
package main
import (
"errors"
"fmt"
)
func readFile(fileName string) error {
if fileName == "main.go" {
return nil
} else {
return errors.New("读取文件失败")
}
}
func myFn() {
defer func() {
err := recover()
if err != nil {
fmt.Println("err:", err)
}
}()
err := readFile("xxx.go")
if err != nil {
panic(err)
}
}
func main() {
myFn()
fmt.Println("继续执行...")
}

2、time包以及日期函数
时间和日期是我们编程中经常会用到的,在golang中time包提供了时间的显示和测量用的函数。
2.1、time.Now()获取当前时间
go
package main
import (
"fmt"
"time"
)
func main() {
timeObj := time.Now()
fmt.Println(timeObj)
year := timeObj.Year()
month := timeObj.Month()
day := timeObj.Day()
hour := timeObj.Hour()
minute := timeObj.Minute()
second := timeObj.Second()
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
}

2.2、Format方法格式化输出日期字符串
格式化的模板为Go的出生时间2006年1月2号15点04分。
2006->年
01->月
02->日
03->时, 03表示12小时制,15表示24小时制。
04->分
05->秒
go
package main
import (
"fmt"
"time"
)
func main() {
timeObj := time.Now()
str1 := timeObj.Format("2006-01-02 15:04:05")
fmt.Println(str1)
str2 := timeObj.Format("2006-01-02 03:04:05")
fmt.Println(str2)
}

2.3、获取当前时间戳
时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总秒数。 它也被称为Unix时间戳(UnixTimestamp)。
go
timeObj := time.Now()
unixtime := timeObj.Unix()
fmt.Println("当前时间戳:", unixtime)
unixNatime := timeObj.UnixNano()
fmt.Println("当前时间戳:", unixNatime)
Unix获取秒级别的时间戳,UnixNano获取纳秒级别的时间戳。
2.4、时间戳转换成日期字符串
go
unixTime := 1748842817
timeObj := time.Unix(int64(unixTime), 0)
var str = timeObj.Format("2006-01-02 15:04:05")
fmt.Println(timeObj)
fmt.Println(str)
使用time.Unix函数用于将时间戳转换成一个时间对象,第一个参数是int64的秒级时间戳,第二个参数是int64的纳秒级时间戳。
2.5、日期字符串转时间戳
go
func time.ParseInLocation(layout string, value string, loc *time.Location) (time.Time, error)
这是该函数的声明,第一个参数表示格式化模板,第二个参数表示要格式化的字符串,第三个参数传入time.Local即可,该函数有两个返回值,第一个返回值就是一个时间对象,然后我们通过这个事件对象调用Unix函数即可获取时间戳。
go
var str = "2025-06-03 09:00:00"
var tmp = "2006-01-02 15:04:05"
timeObj, _ := time.ParseInLocation(tmp, str, time.Local)
fmt.Println(timeObj)
fmt.Println(timeObj.Unix())

2.6、时间间隔和时间操作函数
time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。
time 包中定义的时间间隔类型的常量如下:
go
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute)
}
例如:time.Duration表示1纳秒,time.Second表示1秒。
上面的常量可以搭配时间操作函数来使用,例如Add函数的使用:
go
timeObj := time.Now()
fmt.Println(timeObj)
timeObj = timeObj.Add(time.Hour) // 增加一个小时
fmt.Println(timeObj)

2.7、定时器
1、使用time.NewTicker(时间间隔)来设置定时器。
go
ticker := time.NewTicker(time.Second)
n := 5
for t := range ticker.C {
fmt.Println(t)
n--
if n == 0 {
ticker.Stop()
break
}
}

2、time.Sleep(time.Second)实现定时器。
go
for i := 0; i < 5; i++ {
fmt.Println("我正在执行定时任务...")
time.Sleep(time.Second)
}
