深入理解与使用go之函数与方法–理解与使用
引子
在 Go 语言中,函数被视为一等公民(First-Class Citizens),这意味着函数可以像其他值(比如整数、字符串等)一样被操作、分配和传递。而方法是附加到给定类型的函数。附加类型称为接收器,可以是指针或值。
我们分别看两个例子:
func Print(r int) {
fmt.Println(r)
}
func Add(a, b int) int {
var r int
defer Print(r)
r = a + b
return r
}
func main() {
Add(1, 2)
}
这个打印结果是啥? ,再看下面的例子
type Student struct {
Score int
}
func (s Student) Set(score int) {
s.Score = score
}
func (s Student) Get() int {
return s.Score
}
func main() {
s := &Student{120}
s.Set(88)
fmt.Println(s.Get())
}
这个最终打印结果怎么不是我们希望的那样,我加地址符了哦,那么问题来了
- defer函数的执行逻辑是啥,他和return到底内个先
- 方法接收器我们应该给指针还是值
- 我们应该使用结果命名参数么
- go函数有可变数量参数么,参数是否有默认值
- 泛型参数用处是什么
- 函数里的变量都在栈上么
带着这些问题,我们来讨论讨论今天要说的函数与方法
函数与方法
分类
老规矩,还是先分类,其实显而易见的我们分成了两类
-
函数
-
根据函数入参
- 普通参数
- 可变参数
- 默认值
-
根据返回命名参数
- 返回类型参数
- 返回命名参数
-
init
初始化函数也值得说一说
-
defer
我觉得有必要把
defer
单独拿出来说说,他和return
的关系在某些情况下很难甄别
-
-
方法
- 根据接收器
- 值接收器
- 指针接收器
- 根据接收器
-
构造函数
函数
函数入参
普通参数
func Add(a, b int) int {
return a + b
}
// 调用: Add(1,2)
可变参数
举个栗子
func Add(s ...int) int {
var sum int
for _, v := range s {
sum += v
}
return sum
}
-
可变参数 以同一类型 带3个点 作为入参
-
如果有多个参数,可变参数只能作为最后一个参数
func Add(name string, s ...int) int
-
可变参数调用 可以不传、传1到多个参数
Add() Add(1) Add(1,2) // ...
-
切片传入可变参数,可以使用语法糖
slice := []int{1, 2, 3, 4, 5} Add(slice...)
默认值
默认参数值是指在函数定义中为参数提供一个默认值,如果调用函数时没有提供该参数的值,则使用默认值作为参数值,在 Go 语言中,函数没有直接支持默认参数值的功能。不过,我们可以依赖可变参数来构造一个
func SetProject(serverAddr string, ports ...int) {
var port int
defaultPort := 80
if len(ports) > 0 {
port = ports[0]
} else {
port = defaultPort
}
fmt.Println(port)
// other code here
}
我们调用
func main() {
SetProject("user")
SetProject("user", 8080)
}
这样就解决了 默认值的问题,不过有个弊端,就是我们始终只能有一个默认值参数,如果我们希望有多个呢?
结构体来凑
type Project struct {
ServerAddr string
Port int
}
func SetProject(pro ...Project) {
var project Project
defaultPort := Project{
ServerAddr:"localhost",
Port: 80,
}
if len(pro) > 0 {
project = pro[0]
} else {
project = defaultPort
}
fmt.Println(project.ServerAddr, project.Port)
// other code here
}
返回命名
不带命名
func Add(a, b int) int {
return a + b
}
带命名
func Add(a, b int) (res int) {
res = a + b
return res
}
讨论
事实上,命名结果参数是Go中不常用的选项。如同样的函数体,第1个不命名的参数更简洁明了,那么真的是这样么?
我们看下面这个例子
func GetLocation(addrName string) (float64, float64, error) {
// code here
}
我们查询一个地图,返回经纬度和错误,通常经度在前,纬度在后,确定是如此么,换做其他人习惯不一样呢
如果我们加上命名,是不是函数签名一目了然,我们不必去关系函数体里到底是怎么返回的
func GetLocation(addrName string) (lng,lat float64, err error) {
// code here
}
同样的例子,我们增加上下文
func GetLocation(ctx context.Context, addrName string) (lng, lat float64, err error) {
// 模拟长耗时 触发错误
time.Sleep(2 * time.Second)
if ctx.Err() != nil {
return 0, 0, err
}
return 100, 43.12, nil
}
你有没有看出问题,这里编译没有任何问题,但当我们外部判断返回值时
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
lng, lat, err := GetLocation(ctx, "beijing")
if err != nil {
panic(err)
}
fmt.Println(lng, lat)
额,打印了是0,0,没有panic
, 这是怎么回事,细心的同学可能早就看明白了
if ctx.Err() != nil {
return 0, 0, err
}
我们只是判断了错误,并没有有给错误赋值,这种可能是经常会出现粗心的错误,但是因为编译能通过,很难察觉出来,所以大部分情况下,我们不建议错误命名,如果需要,需要确保检查你的返回值都进行了正确的赋值
我们改下
if err = ctx.Err(); err != nil {
return 0, 0, err
}
init 函数
- 包内执行顺序
init函数是用于初始化应用程序状态的函数。它不需要参数,也不返回任何结果(func()函数)。初始化软件包时,会计算软件包中的所有常量和变量声明。然后,执行init函数。
var loc = func() int {
fmt.Println("var variable")
return 2
}()
func init() {
fmt.Println("init func")
}
func main() {
fmt.Println("main func")
}
执行,打印
var variable
init func
main func
-
多包执行顺序
文件目录如下
├── main.go |── sub | └── sub.go └── add └── add.go
sub,go
package sub import "fmt" func init() { fmt.Println("package sub") } func Sub(a, b int) int { return a - b }
add.go
package add import "fmt" func init() { fmt.Println("package add") } func Add(a, b int) int { return a + b }
main.go
func init() { fmt.Println("package main") } func main() { fmt.Println(sub.Sub(1, 2)) fmt.Println(add.Add(1, 2)) }
运行
package add package sub package main -1 3
很明显,init 函数的执行顺序是,先按包的字母顺序依次执行的,最后是
main
包假设我们改下 main
func main() { fmt.Println(sub.Sub(1, 2)) }
打印
package sub package main -1
这下不执行 add 的init 函数了,也就是不导入的包,我们不运行
-
多个init
有一个很有意思的现象, 正常情况下,单个包里,我们是不允许定义多个重名函数的,而init 可以
func init() { fmt.Println("init 1") } func init() { fmt.Println("init 2") } func main() { fmt.Println("main") }
Stack Overflow有个高分回答,我们在一个大文件里,允许多个
init()
函数使您可以将初始化代码放在它们应该初始化的部分附近,更方便便捷,参考:文章 -
我们何时使用
那么我们何时需要使用
init
函数呢,有一个经典的database/sql
驱动包,假设我们使用mysql
驱动我们可能导入这么写
import ( "database/sql" _ "github.com/go-sql-driver/mysql" )
我们看看做了啥
mysql/driver.go
的init
func init() { if driverName != "" { sql.Register(driverName, &MySQLDriver{}) } }
初始化了驱动,如果我们不做匿名导入,这个包可能在特定情况下不需要导入,上面我们提到过的,不做导入的包,不会执行该包的
init
函数,那么注册初始化就没办法实现因此,我们得到一个大概的结论,如果你需要使用
init
函数- 你是否需要定义一个静态资源变量
- 你对包的导入顺序和init 执行顺序有把握么
如果这两者都没有问题,那么你可以使用,而一般情况下,我们对此保持谨慎态度
defer 函数
defer
关键字用于延迟(defer)函数的执行。通过使用defer
关键字,我们可以确保某个函数在当前函数执行完毕之前被调用。无论函数是正常返回还是发生异常,被延迟的函数都会被执行。
-
带返回值的延迟函数
func t1(i int) (r int) { r = i defer func() { r += 6 }() return r } func t2(i int) (r int) { defer func() { r += i }() return 8 }
打印
func main() { println(t1(1)) println(t2(2)) }
结果
7 10
是不是很意外,我们看下t1执行顺序
- r = i 这时,r = 1
- return 之前,进入延迟函数,r = r + 6
- 然后 return r r此时值为 7,所以结果是 7
再看下 t2 的执行顺序
- return 8 , 这时候相当于间接给 返回值赋值为8 即 r =8
- return 之前,进入延迟函数 r = r + i 即 r = 8+2
- 然后 return r r此时值为 10,所以结果是10
所以结论是,不管我们有没有显示的调用返回变量名,return的值依然会先赋值给结果,然后执行defer,最后再return
-
先执行panic还是先执行defer
func tp() { defer fmt.Println("nihao") fmt.Println("hello") panic("panic tp") }
结果
hello nihao panic: panic tp
如果我们有捕获的情况下,defer执行顺序还是一样的么
func tp() { defer func() { recover() fmt.Println("recover") }() defer fmt.Println("nihao") fmt.Println("hello") panic("panic tp") }
打印
hello nihao recover
结论:
- panic 在defer 执行之后触发
- defer执行顺序都是后进先出
-
使用位置
- 资源释放
http 读取资源释放
resp, err := client.Post(url, "application/json", body)” defer resp.Body.Close()
sql 读取资源释放
rows, err := db.Query("select * from User") if err != nil { return err } // code here defer rows.Close()
文件打开资源释放
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, os.ModeAppend) if err != nil { return err } defer f.Close(); // code here
- 代码块统计 (耗时、内存)
func tp() { var ( startMem runtime.MemStats endMem runtime.MemStats ) runtime.ReadMemStats(&startMem) start := time.Now() defer func() { fmt.Println("cost time ", time.Since(start).Milliseconds()) runtime.ReadMemStats(&endMem) memConsumed := endMem.TotalAlloc - startMem.TotalAlloc fmt.Printf("Memory consumed: %v bytes\n", memConsumed) }() // code here }
- panic错误捕获
func tp() (err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("recovery from %v", p) } }() // code here }
- 其他需要延迟操作的地方
- 扩展
有个很有趣的defer 函数调用链现象
type Slice []int func NewSlice() Slice { return make(Slice, 0) } func (s *Slice) Add(elem int) *Slice { *s = append(*s, elem) fmt.Print(elem) return s } func main() { s := NewSlice() defer s.Add(5).Add(6).Add(7) s.Add(3) }
打印结果是
5,6,3,7
当我们调整
defer s.Add(5).Add(6)
打印结果
5,3,6
再次调整
defer s.Add(5)
打印结果
3,5
然而,当我们加入匿名包后
defer func() { s.Add(5).Add(6).Add(7) }()
打印结果
3,5,6,7
可观测到的结论是:
- defer 后面如果不是跟着匿名函数,会直接执行到只剩一个调用链函数后停下来
- 如果是匿名函数包裹,则按照正常的思维依次执行
感兴趣的可以尝试一下
方法
值接收
回到,我们上面 引子
里提到的例子,不管后续如何调用设置方法,我们的 Score 始终得到的是初始化的值
这就是值接收器带来的特点
- 如果我们必须强制执行接收者的不变性
- 如果接收器是map、func或channel。否则,会出现编译错误。
- 如果接收器是一个基础不可变类型 如 int, float64, 或者 string.
指针接收
那么如果我们需要改变分数呢, 有两个方法
- 使用指针接收器
func (s *Student) Set(score int) {
s.Score = score
}
func (s *Student) Get() int {
return s.Score
}
- 或者我们将可变字段存储为指针类型
type Student struct {
Score *int
}
func (s *Student) Set(score int) {
*s.Score = score
}
func (s *Student) Get() int {
return *s.Score
}
func main() {
a := 120
s := &Student{&a}
s.Set(88)
fmt.Println(s.Get())
}
很显然,指针类型更简单明了
构造函数
为什么要单独说说构造函数呢,有很多人困惑当我们面向对象和设计模式用的越来越多的时候,可扩展性就越来越重要
就像我们前面提到的 我们可能在函数中会遇到可变参、默认参的问题
type Project struct {}
func NewProject(addr string, port int) error {
// code here ...
}
需求如下:
- 1.如果未设置端口,它将使用默认端口。
- 2.如果端口为负数,则返回错误。
- 3.如果端口等于0,则使用随机端口。
- 4.否则,它使用客户端提供的端口。
- 5.如果未设置地址,则使用默认地址
首先 我们需要考虑是否进行了设置,那么意思就是客户可以不传 port 参数
func NewProject(addr string, ports ...int) error {
var port int
defaultPort := 80
if len(ports) > 0 {
port = ports[0]
} else {
port = defaultPort
}
if port < 0 {
return errors.New("port is error")
}
if port == 0 {
port = randPort()
}
}
这样,我们解决了port 的问题,你发现还有个 addr 也可以不传,如上面我们函数所说的,多个可变参数,我们改造结构体
type Project struct {
Port int
Addr string
}
func NewProject(pro ...Project) error {
var p Project
defaultPort := 80
defaultAttr := "localhost"
if len(pro) > 0 {
p = pro[0]
} else {
p = &Project{defaultPort, defaultAttr}
}
if p.Port < 0 {
return errors.New("port is error")
}
if p.Port == 0 {
port = randPort()
}
if len(p.Addr) == "" {
p.Addr = defaultAttr
}
}
好像很完美,但是新的问题来了,如果我们使用过程中,只想改变 addr, port使用默认值,如下
p := NewProject(&Project{Addr:"120.0.9.1"})
那么新的问题来了,第3点,如果端口为0,就产生随机端口,而不设置又应该取默认值
而这里,结构体不设置字段的默认值又是零值,是不是无解,我们改变一下
type Project struct {
Port *int
Addr string
}
如上, 将Port的类型设置为 *int
,这样如果不设置,那么默认就是空指针nil
if p.Port == nil {
*p.Port = defaultPort
}
总觉得差点意思,我们来看看 github.com/go-sql-driver/mysql
包里的 dsn.go
文件
type Config struct {
User string // Username
Passwd string // Password (requires User)
Net string
// code ...
}
// Functional Options Pattern
type Option func(*Config) error
// Apply applies the given options to the Config object.
func (c *Config) Apply(opts ...Option) error {
for _, opt := range opts {
err := opt(c)
if err != nil {
return err
}
}
return nil
}
// 参数处理
func BeforeConnect(fn func(context.Context, *Config) error) Option {
return func(cfg *Config) error {
cfg.beforeConnect = fn
return nil
}
}
根据这个示例,我们可以加强我们的构造函数
type Option func(*Project) error
func NewProject(pro ...Option) error {
p := &Project{}
for _, opt := range Option {
if err := opt(p); err != nil {
return err
}
}
var (
port int
addr string
)
defaultPort := 80
defaultAttr := "localhost"
if p.Port == nil {
port = defaultPort
}
if len(p.Addr) == "" {
p.Addr = defaultAttr
}
if p.Port < 0 {
return errors.New("port is error")
}
if p.Port == 0 {
port = randPort()
}
}
func WithPort(port int) Option {
return func(p *Project) error {
p.Port = p
}
}
func WithAddr(addr string) Option {
return func(p *Project) error {
p.Addr = addr
}
}
调用:
func main() {
opts := []Option{
WithPort(8080),
WithAddr("127.3.4.5"),
}
p, err := NewProject(opts...)
if err != nil {
// code handle
}
}
看起来似乎费劲了许多,反过来想,如果我们增加了新的属性,服务名称 ServerName string
我们是不是不用动现有的所有方法,只需要增加
type Project struct {
// ...
ServerName string
}
func WithServerName(serverName string) Option {
return func(p *Project) error {
p.ServerName = serverName
}
}
而之前已经在用的初始化丝毫没有任何影响
好了,函数与方法的使用就是这些,那么泛型我们什么时候用,变量是分配在栈还是堆将在下一章揭晓。