一、方法与函数

面向对象的叫方法,面向过程的叫函数。

Java作为面向对象的语言,只有方法这个一说。而golang语言却有函数、方法两种说法。在golang语言中方法就是包含了接收者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:

func (variable_name variable_data_type) function_name() [return_type]{
   /* 函数体*/
}

二、go-redis 源码

最近看 go-redis 源码的时候发现代码中有些方法的接收者为函数类型,当时看的时候还是有点懵的,记录如下:

redis版本:v7.4.0

使用redis的业务代码如下:

type RDB struct {
	goRedis *redis.Client
}

// 初始化Redis
func (this *RDB) Init() {
	this.goRedis = redis.NewClient(&redis.Options{
		Addr:         "地址",
		Password:     "redis密码",
		ReadTimeout:  "读超时时间",
		WriteTimeout: "写超时时间",
	})
	_, err := this.goRedis.Ping().Result()
}

//redis使用示例
func (this *RDB) SetValue(key string, value string, expireTime time.Duration) {
	this.goRedis.SetNX(key, value, expireTime)
}

以下是源码部分:

redis.go 文件
// Client is a Redis client representing a pool of zero or more
// underlying connections. It's safe for concurrent use by multiple
// goroutines.
type Client struct {
	*baseClient
	cmdable
	hooks
	ctx context.Context
}

上面这部分代码定义了一个结构体 Client,包含有四个属性,其中 cmdable 在源码中是这样定义的:

commands.go 文件
type cmdable func(cmd Cmder) error

cmdable 是一个函数类型,他在之后将会频繁出现。

初始化 Client 的时候业务代码调用的是 redis.NewClient 方法,以下是 redis.Client 的源码部分:

// NewClient returns a client to the Redis Server specified by Options.
func NewClient(opt *Options) *Client {
	opt.init()

	c := Client{
		baseClient: newBaseClient(opt, newConnPool(opt)),
		ctx:        context.Background(),
	}
	c.cmdable = c.Process

	return &c
}

func (c *Client) Process(cmd Cmder) error {
	return c.ProcessContext(c.ctx, cmd)
}

注意 c.cmdable = c.Process 这行代码,Process 是一个方法,入参为 Cmder,也就睡 redis 操作的具体执行命令,这里将其赋值给了 Client 的 cmdable。

下面的代码是设置 redis K-V的操作,并设置了超时时间。


commands.go文件
// Redis `SET key value [expiration] NX` command.
//
// Zero expiration means the key has no expiration time.
//SetNX 方法的接受者 cmdable 为一个函数类型
func (c cmdable) SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd {
	var cmd *BoolCmd
	if expiration == 0 {
		// Use old `SETNX` to support old Redis versions.
		cmd = NewBoolCmd("setnx", key, value)
	} else {
		if usePrecise(expiration) {
			cmd = NewBoolCmd("set", key, value, "px", formatMs(expiration), "nx")
		} else {
			cmd = NewBoolCmd("set", key, value, "ex", formatSec(expiration), "nx")
		}
	}
        //上面的逻辑只是在构造参数cmd,真正的执行逻辑还是在 cmdable 这个函数类型中
	_ = c(cmd)
	return cmd
}

注意上面的代码块 _ = c(cmd) 之前的代码其实都是在构造 BoolCmd , 真正去执行 SetNX 操作的是 _ = c(cmd) 这行代码。这部分代码也是当时看源码时候的一个疑问点,知道具体的逻辑肯定是在 c(cmd) 里面,但不知道具体在哪实现的。

而 c 是这个方法接受者,是一个函数类型,并没有实际的业务逻辑,真正执行写入逻辑的其实是 Process 函数,在 NewClient 的时候 Client 的 cmdable 就已经是 Process了,这个时候就需要调用函数本体 Process


三、简单例子

上面贴出来的redis源码可能会有点绕,具体的可以自行看下源码,结合IDE可能会更直观些,上面那些代码简化的话也就是下面的样子。方法的接收者是一个函数结构体这种情况之前还没有见到过,初次见的时候还真的有点搞不懂他的执行顺序、逻辑是什么,后面查看了些资料后算下勉强搞懂了,然后记录下来。

package main

import (
	"fmt"
)

func main() {
	NewClient().Say()
}

//定义一个结构体
type Client struct {
	cmdable
}

//定义一个函数类型,类似于上面redis例子的 cmdable 函数
type cmdable func(str string) error

//初始化操作,类似于上面redis例子的 NewClient 方法
func NewClient() *Client{
	c := Client{
	}
	c.cmdable = SayChinese
	return &c
}

//具体逻辑的函数,类似与上面redis例子中的 Process 方法
func SayChinese(str string)  error{
	/*
	do something
	*/
	fmt.Println("输出内容 "+ str)
	return nil
}

//接收者是一个函数类型,实际的执行逻辑是,类似与上面redis例子中的  SetNX 方法
func (c cmdable)Say()  {
	str := "测试"
        //调用c()函数本体
	c(str)
}


四、参考资料: