实现接口的函数类型

在线运行open in new window

// 接口规范
type Runnable interface {
	Run(speed int)
}

// 接口函数类型
type RunnableFunc func(int)

// 实现了 Runnable 接口,内部调用它自己
func (f RunnableFunc) Run(speed int) {
	f(speed)
}

type Cat struct{}

func (c Cat) Run(speed int) {
	fmt.Printf("Cat running by %dm\n", 3*speed)
}

type Dog struct{}

func (d Dog) Run(speed int) {
	fmt.Printf("Dog running by %dm\n", 4*speed)
}

// 业务逻辑要求能 Run 的具体对象来完成工作
func Move(r Runnable, speed int) {
	r.Run(speed)
}

func main() {
	// 1.简单场景可以减少代码量,面向对象和函数式变成结合了起来
	fish := func(speed int) {
		fmt.Printf("Fish running by %dm\n", 1*speed)
	}

	// 把具名函数封装成接口对象
	Move(RunnableFunc(fish), 1)

	// 把匿名函数封装成接口对象
	Move(RunnableFunc(func(speed int) {
		fmt.Printf("Bird running by %dm\n", 2*speed)
	}), 2)

	// 2. 复杂的场景,可能需要传入一个实现了接口的自定义对象
	Move(new(Cat), 3)
	Move(new(Dog), 4)

	/*
		Fish running by 1m
		Bird running by 4m
		Cat running by 9m
		Dog running by 16m
	*/
}

代码实现了一个 Runnable 接口和一个 Move 函数,可以让不同的对象在不同的速度下运动。其中,Runnable 接口规范了一个 Run 方法,Move 函数则接受一个实现了 Runnable 接口的对象和一个速度参数,调用该对象的 Run 方法让其运动起来。

这里使用了一个有趣的技巧:定义一个 RunnableFunc 类型的函数,让其实现 Runnable 接口。这样,我们就可以把一个普通的具名或匿名函数转换为 Runnable 对象,然后传给 Move 函数让其运动起来,这种方法可以减少代码量并且方便使用。

具体实现中,定义了 Cat 和 Dog 两个实现了 Runnable 接口的结构体,分别在 Run 方法中输出不同的文字描述。此外,还定义了两个函数 fish 和一个匿名函数,这些函数都实现了 RunnableFunc 的类型规范。最后,在 main 函数中分别使用不同的参数调用了 Move 函数,让这些对象在不同的速度下运动起来。

接口函数的价值在于即能将普通的函数类型作为实现接口的参数,也能将结构体作为实现了接口的参数,标准库 net/http 的对 handler 函数的定义:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

共享的 error 状态模式

在线运行open in new window

type Person struct {
	Name    string
	Address string
	ZipCode string
}

type PersonScanner struct {
	idx  int
	strs []string
	err  error
}

func NewPersonScanner(s string) *PersonScanner {
	return &PersonScanner{
		idx:  -1,
		strs: strings.Split(s, "|"),
	}
}

// 简化了参数类型,实际情况更多的是一个指针 interface{}/any
func (ps *PersonScanner) read(data *string) {
	// 如果之前就有错误了,每次 read 就直接中断
	if ps.err != nil {
		return
	}

	// 使用一个数组越界的判断简化了实际业务逻辑
	// 真实的场景可能需要很复杂的数据校验和字节拷贝过程
	ps.idx += 1
	if ps.idx >= len(ps.strs) {
		ps.err = errors.New("not enough data")

		return
	}

	*data = strings.TrimSpace(ps.strs[ps.idx])
}

func (ps *PersonScanner) Parse() *Person {
	p := &Person{}

	// 实际业务 Name、Address、ZipCode 可能都是复杂对象
	// 构造/反序列化这些对象的时候可能不符合数据要求
	ps.read(&p.Name)
	ps.read(&p.Address)
	ps.read(&p.ZipCode)

	return p
}

func (ps *PersonScanner) Err() error {
	return ps.err
}

func good() {
	ps := NewPersonScanner("zhangsan |     street 99   | 620000")
	p := ps.Parse()
	if ps.Err() == nil {
		fmt.Printf("%+v\n", p) // &{Name:zhangsan Address:street 99 ZipCode:62721}
	} else {
		fmt.Println(ps.Err())
	}
}

func bad() {
	ps := NewPersonScanner("zhangsan")
	p := ps.Parse()
	if ps.Err() == nil {
		fmt.Printf("%+v\n", p)
	} else {
		fmt.Println(ps.Err()) // not enough data
	}
}

func main() {
	good()
	bad()
}

代码实现了一个 PersonScanner 结构体,它可以从字符串中解析出 Person 结构体的数据。

在 NewPersonScanner 函数中,会将传入的字符串根据 | 进行分割,得到一个字符串数组。在 read 函数中,会将数组中的数据一个一个地读取出来,并通过 strings.TrimSpace 函数进行去除空格的处理,最后将读取的结果存储到传入的指针 data 中。

在 Parse 函数中,会通过调用 read 函数来逐一读取 Name、Address、ZipCode 三个属性的值,并将读取到的值存储到一个新创建的 Person 结构体中。

最后,在 Err 函数中,会返回当前 PersonScanner 中存储的错误信息。

在 good 函数中,演示了一个正常的使用场景。首先,创建了一个 PersonScanner 结构体,然后通过 Parse 函数将字符串解析成 Person 结构体,最后检查了 Err 函数的返回值是否为 nil。

在 bad 函数中,演示了一个错误的使用场景。创建了一个 PersonScanner 结构体,并将只包含一个 Name 属性的字符串传入。此时,调用 Parse 函数时,由于字符串中只包含一个属性的值,所以 read 函数在读取第二个和第三个属性值时都会抛出 not enough data 的错误,最终在调用 Err 函数时,会返回该错误信息。

如果一个业务需要很多步骤,每个步骤操作都需要检查错误的时候会带来 error check hell,使用该 error 状态模式的可以避免,代价就是需要最后调用方检查一下 err 状态,看看标准库 bufio Scanner 的用法:

s := bufio.NewScanner(input)
for s.Scan() { 
    // 
}

// 如果 scan 有错误会提前返回,所以最后总是需要检查错误
if err := s.Err(); err != nil { 
    // 
}

对象的链式调用支持

对象的链式调用是指在对象的成员方法中返回当前对象指针,完全是基于写法上的便利,在 builder 模式中最为典型。

在线运行open in new window

type Person struct {
	Name    string
	Address string
	ZipCode string
}

type PersonBuilder struct {
	p *Person
}

func NewPersonBuilder() *PersonBuilder {
	return &PersonBuilder{p: &Person{}}
}

// 三个 build 过程,使用简单的赋值简化了实际业务
func (pb *PersonBuilder) Name(s string) *PersonBuilder {
	pb.p.Name = s

	return pb
}

func (pb *PersonBuilder) Address(s string) *PersonBuilder {
	pb.p.Address = s

	return pb
}

func (pb *PersonBuilder) ZipCode(s string) *PersonBuilder {
	pb.p.ZipCode = s

	return pb
}

func (pb *PersonBuilder) Build() *Person {
	return pb.p
}

func main() {
	pb := NewPersonBuilder()
	p := pb.Name("zhangsan").Address("stree 99").ZipCode("620000").Build()
	fmt.Println(p) // &{zhangsan stree 99 620000}
}

建造者模式将一个复杂对象的构造过程分解成多个简单对象的构造过程,从而使得构造过程更加灵活,同时也能够避免由于参数过多或参数顺序不当而导致的调用混乱的问题。

在这个示例中,PersonBuilder 封装了 Person 对象的构建过程,它包括了三个 build 方法,分别对应 Name、Address 和 ZipCode 三个属性。每次调用 build 方法之后,就会返回一个指向 Person 对象的指针。这样,我们就可以在构建过程中使用链式调用的方式来设置 Person 对象的各个属性,最后调用 build 方法获取最终的对象。

在 PersonBuilder 中,每个 build 方法都返回一个指向 PersonBuilder 对象的指针,这就允许我们在构建过程中链式调用多个方法,如 pb.Name("zhangsan").Address("stree 99").ZipCode("620000").Build(),这样可以让代码更加简洁、易读,同时也减少了出错的概率。

最终,在 main 函数中我们可以通过 NewPersonBuilder 方法获取一个新的 PersonBuilder 对象,并使用 pb.Name("zhangsan").Address("stree 99").ZipCode("620000").Build() 的链式调用方式来构建 Person 对象。最后我们打印出了构建出来的 Person 对象,可以看到它的三个属性都被正确地设置了。

装饰器增强对象功能

在线运行open in new window

func funcName(i interface{}) string {
	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func log(f func(s string)) func(string) {
	fn := funcName(f)

	return func(s string) {
		fmt.Println(time.Now().Format("2006-01-02") + fn + " started")
		f(s)
		fmt.Println(time.Now().Format("2006-01-02") + fn + " end")
	}
}

func hello(s string) {
	fmt.Println("hello " + s)
}

func main() {
	/*
		2023-03-13main.hello started
		hello zhangsan
		2023-03-13main.hello end
	*/
	f1 := log(hello)
	f1("zhangsan")

	/*
		2023-03-13main.main.func1 started
		hilisi
		2023-03-13main.main.func1 end
	*/
	log(func(s string) {
		fmt.Println("hi" + s)
	})("lisi")
}

程序中定义了一个函数 log,它接收一个函数类型参数 f,并返回一个函数类型参数。返回的函数类型参数中,首先记录下函数 f 的名称及开始时间,然后调用函数 f,并在函数 f 执行完毕后记录下结束时间。通过这种方式可以方便地对函数进行装饰,实现类似日志记录、性能监控等功能。

在示例程序中,定义了一个函数 hello,它接收一个字符串参数,并打印出 "hello " + s 的结果。然后,通过调用 log(hello) 得到一个新的函数 f1,并调用 f1("zhangsan")。这时,函数 hello 将会被调用,执行前后输出时间,这样就可以记录下函数 hello 的开始和结束时间了。另外,在示例程序中还定义了一个匿名函数,它与 hello 函数的功能类似,接收一个字符串参数,并打印出 "hi" + s 的结果。然后,直接通过 log 函数调用该匿名函数,并传递 "lisi" 作为参数,即可实现对该匿名函数的装饰并输出相应的日志信息。

用反射实现通用的函数装饰器

在线运行open in new window

func funcName(i interface{}) string {
	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func hello(s string) {
	fmt.Println("hello " + s)
}

func decorator(dfPtr, f interface{}) {
	df := reflect.ValueOf(dfPtr).Elem()
	tf := reflect.ValueOf(f)
	fn := funcName(f)

	v := reflect.MakeFunc(tf.Type(),
		func(in []reflect.Value) (out []reflect.Value) {
			fmt.Println(time.Now().Format("2006-01-02") + fn + " started")
			res := tf.Call(in)
			fmt.Println(time.Now().Format("2006-01-02") + fn + " end")
			return res
		})

	df.Set(v)
}

func main() {
	/*
		2023-03-14main.hello started
		hello hello
		2023-03-14main.hello end
	*/
	var hf1 func(s string)
	decorator(&hf1, hello)
	hf1("hello")

	/*
	   2023-03-14main.hello started
	   hello hello
	   2023-03-14main.hello end
	*/
	hf2 := hello
	decorator(&hf2, hello)
	hf2("hello")
}

代码实现了一个装饰器,将一个函数包装成带有日志输出的函数。具体来说,它定义了一个 decorator 函数,接受两个参数:第一个参数是一个指向函数指针的指针,第二个参数是函数。在函数体内,它通过反射创建了一个新的函数,该函数包装了原始函数并添加了日志输出的功能。然后,它将新的函数设置为第一个参数指向的函数指针的值。这样,我们就可以通过调用新的函数来调用原始函数并在控制台上输出日志。

在 main 函数中,我们首先定义了一个空函数指针 hf1,然后通过 decorator 函数将其设置为包装了 hello 函数的新函数。最后,我们调用 hf1 函数,实际上调用的是被包装后的 hello 函数。

另外,我们还定义了一个函数指针 hf2,将其设置为 hello 函数的值,然后再次使用 decorator 函数将其包装。然后我们通过调用 hf2 来验证被包装后的 hello 函数是否起作用。

总的来说,这段代码通过反射实现了函数的装饰器,使得我们可以在不修改原始函数的情况下,为其添加额外的功能。

函数式的可选参数配置

通常是 builder 模式的函数式风格重构,通常对参数会重定义一个 Option 函数式类型。

在线运行open in new window

type Person struct {
	Name    string
	Address string
	ZipCode string
}

type Option func(*Person)

func Address(s string) Option {
	return func(p *Person) {
		p.Address = s
	}
}

func ZipCode(s string) Option {
	return func(p *Person) {
		p.ZipCode = s
	}
}

// 假设 name 对业务上是必须的,Address 和 ZipCode 是可选的
func NewPerson(name string, opts ...Option) *Person {
	p := &Person{Name: name}

	for _, opt := range opts {
		opt(p)
	}

	return p
}

func main() {
	p1 := NewPerson("zhangsan", Address("street 99"), ZipCode("620000"))
	fmt.Println(p1) // &{zhangsan street 99 620000}

	p2 := NewPerson("lisi")
	fmt.Println(p2) // &{lisi  }
}

程序通过定义 Option 类型的函数,使用可变参数列表传入,将需要修改的参数值以函数的形式传入,从而实现在构造函数中传入可选参数的效果。

具体实现中,我们定义了一个 NewPerson 构造函数,该函数首先会为必须的参数赋值,然后通过循环调用可选参数的函数,将可选参数的值应用到对象中。这样在构造函数中调用时,可以传入必须的参数,也可以传入多个可选参数,也可以不传任何可选参数,从而实现了可选参数的效果。

在实现中,我们使用了 Go 语言中的闭包,将需要修改的参数值以函数的形式保存下来。通过遍历可选参数列表,逐个将参数函数应用到对象上,从而实现了可选参数的功能。

需要注意的是,对于一些不可选的参数,我们需要在构造函数中指定其值。同时,为了避免可选参数在使用时位置混乱,我们通常将可选参数放在参数列表的最后面。

高阶函数的迭代处理器

在线运行open in new window

type Calculator func(n int) int

func mapFunc(nums []int, f Calculator) {
	for idx, n := range nums {
		nums[idx] = f(n)
	}
}

type Counter func(s string) int

func reduceFunc(strs []string, f Counter) int {
	count := 0

	for _, s := range strs {
		count += f(s)
	}

	return count
}

func main() {
	nums := []int{1, 2, 3}
	mapFunc(nums, func(n int) int {
		return n * n
	})
	fmt.Println(nums) // [1 4 9]

	mapFunc(nums, func(n int) int {
		return 2 * n
	})
	fmt.Println(nums) // [2 8 18]

	strs := []string{"this", " is", " a", " test"}
	count := reduceFunc(strs, func(s string) int {
		return len(s)
	})
	fmt.Println(count) // 14

	count = reduceFunc(strs, func(s string) int {
		return len(strings.TrimSpace(s))
	})
	fmt.Println(count) // 11
}

程序定义了两个高阶函数,mapFunc 和 reduceFunc,分别对应了函数式编程中的映射和折叠操作。

mapFunc 接收一个整数切片和一个函数,将该函数应用到整数切片中的每一个元素上,生成一个新的整数切片。

reduceFunc 接收一个字符串切片和一个函数,将该函数应用到字符串切片中的每一个元素上,最后累加计算所有元素的计数值。

在 main 函数中,定义了两个匿名函数作为参数传递给这两个高阶函数。第一个匿名函数将整数的平方作为结果,第二个匿名函数将整数乘以 2 作为结果。第一个函数被应用到整数切片中,生成一个新的整数切片,第二个函数又被应用到同一个整数切片中,又生成了一个新的整数切片。

对于字符串切片,第一个匿名函数返回字符串的长度,第二个匿名函数返回去掉首尾空格之后字符串的长度,这两个函数分别被应用到字符串切片中,得到了两个不同的计数值。

高阶函数组成的管道处理器

在线运行open in new window

type NormalizeFunc func([]int) <-chan int
type PipelineFunc func(<-chan int) <-chan int

// convert slice to chan
func norm(nums []int) <-chan int {
	// no buffer chan
	out := make(chan int)

	go func() {
		for _, n := range nums {
			out <- n
		}

		// trigger next range finish
		close(out)
	}()

	return out
}

// select odd numbers
func odd(in <-chan int) <-chan int {
	// no buffer chan
	out := make(chan int)

	go func() {
		// wait last pipe to close
		for n := range in {
			if n%2 != 0 {
				out <- n
			}
		}

		close(out)
	}()

	return out
}

// sum of chan
func sum(in <-chan int) <-chan int {
	// no buffer chan
	out := make(chan int)

	go func() {
		sum := 0

		// wait last pipe to close
		for n := range in {
			sum += n
		}

		out <- sum
		close(out)
	}()

	return out
}

// call func in pipe
func pipeline(nums []int, norm NormalizeFunc, pfs ...PipelineFunc) <-chan int {
	ch := norm(nums)

	for _, pf := range pfs {
		ch = pf(ch)
	}

	return ch
}

func main() {
	nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	ch := pipeline(nums, norm, odd, sum)

	// input: n -> ouput: 1
	for n := range ch {
		fmt.Println(n)
	}
}

代码实现了一个管道(pipeline)的功能,它可以对一个整数列表进行处理,最终输出一个数字。代码中定义了三个函数:norm、odd 和 sum,它们分别用于将整数列表转换成 channel、选出其中的奇数,以及对 channel 中的数字进行求和。这三个函数都接受一个 channel 作为参数,返回一个 channel。pipeline 函数则接受一个整数列表和若干个 PipelineFunc 类型的函数作为参数,最终返回一个 channel。在 pipeline 函数中,先使用 norm 函数将整数列表转换成 channel,然后依次调用各个 PipelineFunc 函数对 channel 进行处理,最终返回处理后的 channel。在 main 函数中,调用 pipeline 函数对整数列表进行处理,并输出最终的结果。

Map-Reduce 函数处理器

在线运行open in new window

func double(nums []int) <-chan int {
	ch := make(chan int, len(nums))

	for _, n := range nums {
		ch <- n * 2
	}

	close(ch)
	return ch
}

func doMapReduce(nums []int, threads int, biz func([]int) <-chan int) int {
	// compute workers
	tasks := len(nums)
	parts := tasks / threads
	if tasks%threads > 0 {
		parts += 1
	}

	wg := &sync.WaitGroup{}
	outs := []<-chan int{}

	// partition
	for i := 0; i < parts; i++ {
		wg.Add(1)

		go func(idx int) {
			defer wg.Done()

			start := idx * threads
			end := (idx + 1) * threads
			if end > tasks {
				end = tasks
			}

			outs = append(outs, biz(nums[start:end]))
		}(i)
	}

	wg.Wait()

	// merge
	total := 0
	for _, out := range outs {
		for n := range out {
			total += n
		}
	}

	return total
}

func main() {
	nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

	s1 := doMapReduce(nums, 2, double)
	s2 := doMapReduce(nums, 3, double)
	s3 := doMapReduce(nums, 4, double)

	fmt.Println(s1, s2, s3) // 90 90 90
}

程序先定义了一个 double 函数,它接受一个整数切片 nums,将其中每个元素都翻倍,然后返回一个通道,包含了所有翻倍后的元素。函数实现中,先创建一个通道 ch,容量为整数切片 nums 的长度,然后遍历整数切片 nums 中的每个元素,将其翻倍后写入通道 ch 中,最后关闭通道 ch 并返回它。这个函数可以看作是 Map 阶段的具体实现。

接下来是 doMapReduce 函数,它接受一个整数切片 nums、一个线程数 threads 和一个函数 biz,它的返回值是一个整数,表示 biz 函数对输入整数切片 nums 进行 Map 处理后的结果。函数实现中,首先计算出将输入整数切片 nums 分成若干个子切片需要的数量 parts,然后创建一个等待组 wg 和一个存放通道输出的切片 outs。接着,使用 for 循环将输入整数切片 nums 分成 parts 个子切片,对每个子切片启动一个协程进行 Map 处理,将所有子切片的输出都写入 outs 中。最后使用 for 循环将所有通道输出的值相加得到最终的结果,并返回它。

最后是程序的 main 函数,它先定义了一个输入整数切片 nums,然后分别以 2、3 和 4 个线程数调用 doMapReduce 函数,将每个线程数下的结果分别存储在变量 s1、s2 和 s3 中,并将它们输出到标准输出。由于 double 函数将输入整数切片中的每个元素都翻倍,所以最终的结果都应该是 90。

Last Updated:
Contributors: Bob Wang