【Golang】:函数和包

目录

[1. 函数](#1. 函数)

[1.1 函数定义](#1.1 函数定义)

[1.2 函数传参](#1.2 函数传参)

[1.3 函数返回值](#1.3 函数返回值)

[1.4 可变参数](#1.4 可变参数)

[1.5 函数类型](#1.5 函数类型)

[1.6 匿名函数](#1.6 匿名函数)

[1.7 defer 延迟调用](#1.7 defer 延迟调用)

[1.8 闭包(Closure)](#1.8 闭包(Closure))

[2. 包](#2. 包)

[2.1. 基本概念](#2.1. 基本概念)

[2.2 使用方式](#2.2 使用方式)

[2.3 init 函数](#2.3 init 函数)


1. 函数

1.1 函数定义

复制代码
func 函数名(参数列表) 返回值类型 {
    // 函数体
    return 返回值
}

注意: Go中的函数不支持函数重载

复制代码
package main

import "fmt"

func add(a int, b int) int {
	return a + b
}
func main() {

	fmt.Println(add(1, 2))

}

1.2 函数传参

参数传递的方式有两种:

  • 值传递:传参时传递的是值的拷贝,函数内部对参数的修改不会影响到原始数据,值类型参数默认采用的就是值传递,包括基本数据类型、数组和结构体。

  • 引用传递:传参时传递的是地址的拷贝,在函数内部对参数的修改会影响到原始数据,引用类型参数默认采用引用传递,包括指针、切片、管道、接口等。

    package main

    import "fmt"

    // 值传递,无法改变内部的数据
    func swap(a int, b int) {
    temp := a
    a = b
    b = temp
    }
    func main() {
    var a int = 100
    var b int = 200
    fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200
    swap(a, b)
    fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200

    }

引用传参:

复制代码
package main

import "fmt"

func swap(a *int, b *int) {
	temp := *a
	*a = *b
	*b = temp
}
func main() {
	var a int = 100
	var b int = 200
	fmt.Println("a: ", a, "b: ", b) // a:  100 b:  200
	swap(&a, &b)
	fmt.Println("a: ", a, "b: ", b) // a:  200 b:  100

}

1.3 函数返回值

1. 返回多个值

Go中函数支持返回多个值,通过返回值列表指明各个返回值的类型即可。如下:

复制代码
package main

import "fmt"

func GetSumAndSub(a int, b int) (int, int) { // 返回多个值
	add := a + b
	sub := a - b
	return add, sub
}

func main() {
	add, sub := GetSumAndSub(10, 20)
	fmt.Println(add) // 30
	fmt.Println(sub) // -10
}

2. 忽略返回值

如果函数返回多个值,在接收时,可以通过_(占位符)忽略不需要的返回值。如下:

复制代码
package main

import "fmt"

func GetSumAndSub(a int, b int) (int, int) { // 返回多个值
	add := a + b
	sub := a - b
	return add, sub
}

func main() {
	_, sub := GetSumAndSub(10, 20)
	fmt.Println(sub) // -10
}

3. 返回值命名

Go中函数支持在返回值列表给返回值命名,这时函数在返回时无需在return后指明需要返回的值,可以避免返回顺序出错。如下:

复制代码
package main

import "fmt"

func GetSumAndSub(a int, b int) (add int, sub int) { // 返回多个值
	add = a + b
	sub = a - b
	return
}

func main() {
	add, sub := GetSumAndSub(10, 20)
	fmt.Println(add) // 30
	fmt.Println(sub) // -10
}

1.4 可变参数

参数数量不确定时使用:

  • 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表的最后。

    package main

    import "fmt"

    func sum(nums ...int) int {
    total := 0
    for _, v := range nums {
    total += v
    }
    return total
    }

    func main() {
    fmt.Println(sum(1)) // 1
    fmt.Println(sum(1, 2)) // 3
    fmt.Println(sum(1, 2, 3)) // 6
    fmt.Println(sum(1, 2, 3, 4)) // 10
    }

1.5 函数类型

在Go中函数也是一种数据类型,可以将其赋值给一个变量,然后通过该变量即可对函数进行调用。如下:

复制代码
package main

import "fmt"

func add(num1 int, num2 int) int {
	return num1 + num2
}

func main() {
	sumFunc := add
	fmt.Printf("类型:%T, 值:%d", sumFunc, sumFunc(1, 2))    // 类型:func(int, int) int, 值:3
}

类型定义和类型别名

1. 类型定义

  • 这里 MyInt 是一个新类型 ,它的底层类型是 int,但是编译器认为 intMyInt 是两个不同的类型。

  • 所以直接赋值会报错,必须 显式类型转换

    package main

    import "fmt"

    func main() {
    type myInt int

    复制代码
      var a myInt = 100
      var b int = 200
      fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:main.myInt, 值:100
      fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200
    
      // a = b     // 错误,
      a = myInt(b)
      fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:main.myInt, 值:200
      fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200

    }

2. 类型别名

  • 这里 MyInt 只是 int别名,两者完全等价,编译器认为它们就是同一个类型。

  • 因此可以直接赋值,不需要转换

    package main

    import "fmt"

    func main() {
    type myInt = int

    复制代码
      var a myInt = 100
      var b int = 200
      fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:int, 值:100
      fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200
    
      a = b                             // 可以直接赋值
      fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:int, 值:200
      fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200

    }

在Go中将函数作为形参也是常见的用法,这时结合自定义数据类型给函数类型取别名,能有效提高代码的可读性。如下:

复制代码
package main

import "fmt"

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

type SumType func(int, int) int // 自定义数据类型

func MyFunc(f SumType, num1 int, num2 int) int {
	return f(num1, num2)
}

func main() {
	result := MyFunc(Sum, 10, 20)
	fmt.Printf("值:%d\n", result) // 值:30
}

1.6 匿名函数

如果希望某个函数只使用一次,可以考虑使用匿名函数,定义时直接使用,不必命名:

复制代码
    func(a int, b int) int{
		return a + b
	}()

可以赋值给变量,进行使用:

复制代码
package main

import "fmt"

func main() {
	// 定义匿名函数并调用
	sum1 := func(a int, b int) int {
		return a + b
	}(1, 2)
	fmt.Printf("类型:%T, 值:%d\n", sum1, sum1) // 类型:int, 值:3

	// 定义匿名函数
	sum2 := func(a int, b int) int {
		return a + b
	}
	fmt.Printf("类型:%T, 值:%d\n", sum2, sum2(100, 200)) // 类型:func(int, int) int, 值:300
}

1.7 defer 延迟调用

1. defer 的作用:用来延迟执行某些操作,通常是释放资源

  • 文件关闭(file.Close()
  • 解锁(mu.Unlock()
  • 数据库连接释放
  • 网络连接关闭

这样写可以避免忘记释放资源,即使函数发生 panicdefer 语句依然会执行。

2. 执行顺序

  • 当程序执行到 defer 时,不会立刻执行 ,而是把该语句 压入 defer 栈

  • 当函数返回时,Go 会按照 后进先出(LIFO) 的顺序执行这些语句。

  • defer 语句的参数会在声明时就被求值,而不是在函数结束时。

    package main

    import "fmt"

    func demo() {
    defer fmt.Println("A")
    defer fmt.Println("B")
    fmt.Println("C")
    }

    func test() {
    x := 10
    defer fmt.Println("defer x =", x) // 保存的是 x=10
    x = 20
    fmt.Println("x =", x)
    }

    func main() {
    demo() // C B A
    test() // x = 20 defer x = 10
    }

常见应用场景

  • 在Go中通常会在创建资源后,通过defer语句将资源关闭,由于defer语句会在当前函数执行完毕后再执行,因此在defer语句之后仍然可以使用创建的资源。

  • 在其他语言中,资源释放时机是一个常见的问题,而Go中的defer机制就使得资源的创建和释放可以成对存在,程序员再也不用担心资源释放时机的问题了。

    func FileOperation(filename string) (err error) {
    file, err := os.Open(filename) // 打开文件
    if err != nil {
    fmt.Printf("open file error, err = %v\n", err)
    return
    }
    defer file.Close() // 关闭文件

    复制代码
      // 进行文件操作...
    
      return

    }

注意事项(常见坑)

  • 多个 defer 的执行顺序是反向的(栈结构)

  • defer 的参数在声明时就确定了,不会随着变量变化而改变

  • defer 与 return 结合时 ,如果 defer 修改了返回值(需要命名返回值),可能导致结果不同。

    package main

    import "fmt"

    func f() (x int) {
    defer func() { x++ }()
    return 3
    }

    func main() {
    x := f()
    fmt.Println("值:%d\n", x) // 4
    }

1.8 闭包(Closure)

1. 什么是闭包?

  • 闭包 = 函数 + 外部变量引用环境

  • 它允许函数"记住"并操作其外部作用域中的变量,即使该作用域已经结束。

    package main

    import "fmt"

    func add(a int) func(int) int {
    var sum = a
    return func(x int) int {
    sum += x
    return sum
    }
    }

    func main() {
    var posSum = add(10)
    fmt.Printf("类型:%T\n", posSum) // 类型:func(int) int
    fmt.Printf("值:%d\n", posSum(10)) // 值:20
    fmt.Printf("值:%d\n", posSum(20)) // 值:40
    fmt.Printf("值:%d\n", posSum(30)) // 值:70

    }

  • sum 是外部变量,func(x int) int 是内部函数。

  • 即使 add() 执行完毕,sum 依然被闭包函数捕获并存储在内存中。

注意:闭包记住环境,变量延长寿命;引用而非拷贝,循环要格外小心。

2. 包

2.1. 基本概念

  1. 包 = 代码的最小组织单元

  2. 每个 Go 源文件都必须声明一个 package

  3. 同一个目录下的 .go 文件必须属于同一个包(除非是 main 包)。

  4. 首字母大写:对外可见(公有)。

  5. 首字母小写:仅包内可见(私有)。

  6. 可管理性: 大项目通常会按功能划分多个包,例如:modelscontrollersservices

2.2 使用方式

包的使用方式可以分为四部:打包导入包给包取别名使用包

1. 打包

复制代码
// 在 .go 文件的第一行写上
package 包名

// 注意:同一目录下的 .go 文件必须属于同一个包。

2. 导入包

复制代码
// 在需要使用的地方通过 import 引入
import "test_go/hello"

3. 给包取别名

注意:取了别名就只能用别名

复制代码
package main

import (
	"fmt"
	h "test_go/hello"     // 可以避免包名冲突,或缩短使用
)

func main() {
	fmt.Println(h.Add(1, 2))
}

4. 使用包

复制代码
// 通过 包名.标识符 调用
// 注意:只有 首字母大写 的函数/变量/类型才能被外部包访问。
package main

import (
	"fmt"
	h "test_go/hello"
)

func main() {
	fmt.Println(h.Add(1, 2))
}

2.3 init 函数

1. 基本概念

  • 每个 Go 源文件都可以包含 一个或多个 init 函数。

  • init 函数在 程序运行前自动调用 ,且 main() 之前执行

  • 不能被显式调用 ,即你不能在代码里写 init()

2. 执行顺序

  1. **包级变量初始化:**先初始化包里的全局变量。

  2. 执行 init 函数: 包中可能有多个 init,会按照它们在源码中出现的顺序执行,但这并不会产生重定义报错,因为init函数在编译阶段会被编译器处理为特殊的符号,确保所有init函数被正确执行而不会发生冲突。

  3. 导入包的 init

    • 如果 main 包依赖其它包,会先初始化依赖包(递归执行)。

    • 即:先初始化被导入的包,再初始化当前包

  4. 最后才执行 main()

    依赖包变量初始化 → 依赖包 init() → 当前包变量初始化 → 当前包 init() → main()

    package main

    import (
    "fmt"
    )

    var num = initNum()

    func initNum() int {
    fmt.Println("初始化全局变量 num")
    return 100
    }
    func init() {
    fmt.Println("init1() 被调用")
    }
    func init() {
    fmt.Println("init2() 被调用")
    }

    func main() {
    fmt.Println("main() 被调用")
    fmt.Println("num =", num)
    }

    // 初始化全局变量 num
    // init1() 被调用
    // init2() 被调用
    // main() 被调用
    // num = 100

如果 main 包导入了其他包,main包初始化之前,会先对其导入的包进行初始化。

复制代码
package main

import (
    "fmt"
    h "test_go/hello"
)

var num = initNum()

func initNum() int {
    fmt.Println("初始化全局变量 num")
    return 100
}
func init() {
    fmt.Println("init() 被调用")
}

func main() {
    fmt.Println("main() 被调用")
    fmt.Println("num =", num)
    fmt.Println(h.Add(1, 2))
}
/*
hello.go中init() 被调用
初始化全局变量 num
init() 被调用
main() 被调用
num = 100
3
*/
相关推荐
程序员码歌1 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
七七&5562 小时前
2024年08月13日 Go生态洞察:Go 1.23 发布与全面深度解读
开发语言·网络·golang
java坤坤2 小时前
GoLand 项目从 0 到 1:第八天 ——GORM 命名策略陷阱与 Go 项目启动慢问题攻坚
开发语言·后端·golang
健康平安的活着3 小时前
java之 junit4单元测试Mockito的使用
java·开发语言·单元测试
bobz9653 小时前
GPT-4.1 对比 GPT-4o
后端
Java小白程序员3 小时前
Spring Framework :IoC 容器的原理与实践
java·后端·spring
小小愿望3 小时前
前端无法获取响应头(如 Content-Disposition)的原因与解决方案
前端·后端
DjangoJason4 小时前
C++ 仿RabbitMQ实现消息队列项目
开发语言·c++·rabbitmq
向日葵.4 小时前
fastdds.ignore_local_endpoints 属性
服务器·网络·php