深入理解与使用go之函数与方法--使用

深入理解与使用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函数有可变数量参数么,参数是否有默认值
  • 泛型参数用处是什么
  • 函数里的变量都在栈上么

带着这些问题,我们来讨论讨论今天要说的函数与方法

函数与方法

分类

老规矩,还是先分类,其实显而易见的我们分成了两类

  1. 函数

    • 根据函数入参

      • 普通参数
      • 可变参数
      • 默认值
    • 根据返回命名参数

      • 返回类型参数
      • 返回命名参数
    • init

      初始化函数也值得说一说

    • defer

      我觉得有必要把 defer 单独拿出来说说,他和 return 的关系在某些情况下很难甄别

  2. 方法

    • 根据接收器
      • 值接收器
      • 指针接收器
  3. 构造函数

函数

函数入参
普通参数
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 函数
  1. 包内执行顺序

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
  1. 多包执行顺序

    文件目录如下

    ├── 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 函数了,也就是不导入的包,我们不运行

  2. 多个init

    有一个很有意思的现象, 正常情况下,单个包里,我们是不允许定义多个重名函数的,而init 可以

    func init() {
    	fmt.Println("init 1")
    }
    func init() {
    	fmt.Println("init 2")
    }
    
    func main() {
    	fmt.Println("main")
    }
    

    Stack Overflow有个高分回答,我们在一个大文件里,允许多个init()函数使您可以将初始化代码放在它们应该初始化的部分附近,更方便便捷,参考:文章

  3. 我们何时使用

    那么我们何时需要使用 init 函数呢,有一个经典的 database/sql 驱动包,假设我们使用 mysql 驱动

    我们可能导入这么写

    import (
    	"database/sql"
    	_ "github.com/go-sql-driver/mysql"
    )
    

    我们看看做了啥

    mysql/driver.goinit

    func init() {
    	if driverName != "" {
    		sql.Register(driverName, &MySQLDriver{})
    	}
    }
    

    初始化了驱动,如果我们不做匿名导入,这个包可能在特定情况下不需要导入,上面我们提到过的,不做导入的包,不会执行该包的 init 函数,那么注册初始化就没办法实现

    因此,我们得到一个大概的结论,如果你需要使用 init 函数

    • 你是否需要定义一个静态资源变量
    • 你对包的导入顺序和init 执行顺序有把握么

    如果这两者都没有问题,那么你可以使用,而一般情况下,我们对此保持谨慎态度

defer 函数

defer关键字用于延迟(defer)函数的执行。通过使用defer关键字,我们可以确保某个函数在当前函数执行完毕之前被调用。无论函数是正常返回还是发生异常,被延迟的函数都会被执行。

  1. 带返回值的延迟函数

    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

  2. 先执行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执行顺序都是后进先出
  3. 使用位置

    • 资源释放

    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
    }
    
    • 其他需要延迟操作的地方
    1. 扩展

    有个很有趣的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
  }
}

而之前已经在用的初始化丝毫没有任何影响

好了,函数与方法的使用就是这些,那么泛型我们什么时候用,变量是分配在栈还是堆将在下一章揭晓。

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值