Golang中如何实现通用包的设计

Golang中如何实现通用包的设计 假设我正在编写一个Go包,旨在优化某些东西,但用户可以定义自己的优化函数和另一个用于检查条件的函数。这些函数可能各不相同,并且在不同问题中可能具有不同的签名。最终,用户将使用一个大型结构体,该结构体将包含指向这两个函数的指针,但问题在于这些函数可以使用不同的参数……例如,一个问题可能使用 func(x, y int) float64func(x, y, z int) bool,而另一个问题可能使用 func(x []int) float64func(x []float64, y bool, c float64) bool。因此,我无法使用一个包含两个函数指针的结构体,因为我必须精确定义函数的签名。坦率地说,我不知道该如何处理这个问题。

我知道上面的描述有点模糊,但我希望您能理解我的意思。您有什么想法可以解决这类问题吗?


更多关于Golang中如何实现通用包的设计的实战教程也可以访问 https://www.itying.com/category-94-b0.html

10 回复

非常感谢。这确实有帮助,尽管我必须承认,至少目前,我的想象力还无法让我构思出代码的最终设计。其中一个原因是我是一个Go语言新手,所以一些问题对我来说相当困难。您是否了解一些围绕这种理念构建的标准库包?

更多关于Golang中如何实现通用包的设计的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


哈!这看起来是一个非常好的主意,甚至适用于比我遇到的更通用的场景。太棒了!我会记住的。首先,我会尝试使用一个结构体,因为在我的情况下,我认为所有东西都可以通过嵌入在多个结构体中的一个结构体中的数据来定义。但你的想法在非常通用的场景下看起来确实很有前景。谢谢!

经过一番思考,我想到了一个不同的方案。整个情况通过几个嵌入在一个大结构体中的结构体来描述。所有不同形式的数据都收集在这个结构体中。因此,我之前提到的那两个函数可以成为这个大结构体的方法,并分别返回 float64bool

现在看起来如此显而易见,以至于我真的很惊讶自己之前没有想到这个方案。这个案例研究极大地展示了代码设计的重要性。

针对你的情况,我目前的方法是定义一个“通用”的数据结构,它只包含多个解析接口(输入)和打印接口(输出);这个数据结构是为你的优化函数量身定制的。

然后我将这个数据结构作为输入传递给一个单一的优化函数,并使用指针来用输出值更新其内部数据结构。

这种方法使我能够:

  1. 将算法与任何输入解析的变更隔离开来。
  2. 通过调整数据结构,轻松管理解析器和打印器的创建/弃用。
  3. 允许我在需要时定义优化函数的接口。
  4. 允许我进行优化/重构,而不影响对外公开的接口(例如解析、打印和优化函数)。

这对你有帮助吗?

信不信由你,今天早上从疲惫的睡眠中醒来后,我的脑海中突然冒出了下面这段代码:

type Data struct {
    Float float64
    Integer int64
    FloatList []float64
    IntList []int64
    ... any other data type that you believe your user will provide ...
}

func MySecretRecipe(input ...*Data) (output *YourOutputType, err error) {
    ...
}

由于我不了解你的算法,所以不确定这是否对你的情况有帮助。但我非常确定,用户可以填写任意数量的 input,并且对于每个 input,用户都可以指定任何已知的数据类型。

至于在算法内部的操作,我通常会将 Data 类型扁平化为一种通用类型(要么使用 float64,这会为了大数值容量而牺牲精度;要么使用位移后的 int64 来保证精度但数值容量较小)。在任何情况下,big.Int 都可以作为最后的手段提供帮助。

非常感谢。我会仔细研究这个。

我认为这个想法本身并不像我最初感觉的那么复杂,但由于我来自相当不同的语言背景(Python 和 R),我仍然需要转变我的思维方式(例如,在 Python 中,实现这个会非常容易)。举个例子,我之前从未使用过接口,虽然我理解其基本概念,但我不仅需要理解它,还需要找到那种“感觉”,以便能够设计更复杂的结构。

不过,这里有一个问题。我认为,对于我提到的那个包,我可能需要将实现这个想法的相当一部分编码工作交给用户来完成。这是因为用户需要能够定义他们自己的、用于优化的函数,并且这些函数可能需要检查各种条件。

正是出于这个原因,我无法编写你提到的那些解析器(a.ParseInputTypeA(...)a.ParseInputTypeB(...)a.ParseInputTypeAV2(...)),而是需要由用户来完成。因为这不仅仅是适应几种可能使用的类型,而是要适应无限多种类型。(嗯,也许不是无限的,但确实会有很多。)例如,我自己马上就能想到一些函数,它们使用两个 []float64 切片和一个 float64,或者使用多个 []float64 和两个 []int 切片,以及这些情况的各种组合。但整个想法是让这个包更加通用,以便用户能够将其用于一组完全不同的两个函数。我会涵盖几种常见情况,但我确实希望给用户一个机会,让他们能够使用任何他们想要的函数。

hollowaykeanho:

顺便欢迎来到Go的世界。🚀🎆 你不会后悔的。

我知道我不会后悔的!它已经是我最喜欢的语言了,虽然我也不知道为什么!就是喜欢它,可能是因为它的简洁,也可能是因为性能,不太确定。我确实有些怀念泛型,而且我知道它即将到来。

我确实希望很快能看到泛型,因为目前这是我唯一怀念的东西。不过,在很多情况下,通过良好的代码设计,我们即使没有泛型也能做出好东西。但确实存在像我们正在讨论的这种情况,Go可能对我们限制得太多了。目前我写这个包是为了学习,但最终我确实想写完并发布它。因此,我可能会等待泛型进入Go 2。我希望这确实能让设计这样一个包变得更简单。

当然,我实际上可以通过实现大约10种常见情况来让整个想法不那么通用,仅此而已。但这意味着,即使只是稍微调整算法(恐怕这里会是常见情况),用户也需要修改相当多的代码。如果是这样的话,我认为在Go中编写这个包就没有意义了。我想给用户一个易于使用的Go包,而不是一个会带来大量工作的东西(而且为了调整它,用户可能需要理解包代码的相当一部分,这需要相当多的时间)。也许Go并不是针对这种情况设计的,我理解这一点。但我非常希望它能在这种情况下工作!我可以轻松地在R或Python中编写这个东西,但我非常想在Go中编写它!然后我可以轻松地将其制作成一个包(如果有人想调整它)和一个用于常见情况优化的小型便携式命令行程序。

我想你们中的许多人不会对我想用Go编程! 感到惊讶 😄

但整个想法是让这个包更加通用,以便用户能够将其用于完全不同的两个函数集。

目前,Go 2 正在开发纯泛型功能。你可能需要在取舍之间做出决定,因为 Go 1 始终追求的是清晰性。

我需要将实现这个想法的相当一部分编码工作交给用户。这是因为用户需要能够定义自己的函数用于优化,并附带各种需要检查的条件。

正是由于这个原因,我无法编写你提到的那些解析器(a.ParseInputTypeA(...)a.ParseInputTypeB(...)a.ParseInputTypeAV2(...)),而是需要用户自己来完成。因为这不仅仅是适应几种可能使用的类型,而是要适应无限多种类型。

Go 提供了接口(Go by Example: Interfaces)、函数值(A Tour of Go)和可变参数函数(Go by Example: Variadic Functions)供你使用。还有一个数学包(big package - math/big - Go Packages),以防你需要一个用于大数操作的通用基础数据类型。

如果以上所有方法都无法应对这种复杂性,你可能需要稍微调整一下你的算法。

我来自相当不同的语言(Python 和 R)

哈哈。是不是对“魔法”上瘾了?:joy: 我以前也是 Python 和 Ruby 开发者。别担心。是的,需要改变不少。Go 非常像 C 语言:注重细节且清晰。

顺便欢迎来到 Go 的世界。:rocket: :fireworks: 你不会后悔的。

你可以查看TLS包的公共部分(common.go)(https://golang.org/src/crypto/tls/)。如你所知,TLS支持大量密码套件,其中一些变体包含各自特定的配置(例如RSA与椭圆曲线)。

对于客户端,你可以查看net/http包(特别是net/http/transport.go https://golang.org/src/net/http/transport.go)来了解tls.Config是如何被部署的。

TLS使用Config数据结构在其自身包内保持一个通用的输入,这样每当有新的变化引入时,它们只需要相应地更新数据结构,而不会干扰其他部分。例如,目前许多人仍在使用TLS1.2,而TLS1.3仍在开发中。当TLS1.3准备就绪时,HTTP包的“客户”只需在他们那一侧更新两个标志(Config.MinVersionConfig.MaxVersion)即可。他们实际上并不深入(除了安全分析师和维护者)了解TLS1.3的内部工作原理。

这个想法并不难理解。整个思路是从“架构”角度思考(这个词可能不太准确,但基本上是将你的思维稍微放大一点):

  1. 从一个以通用数据结构作为输入的函数开始。
  2. 然后使用Go接口方法,用所有的I/O变体来扩展这个通用数据结构。

在你的情况下,你的通用数据结构很可能会有私有数据变量,这样你只想向公众公开那些接口方法。流程将是:

  1. 客户创建“Data”结构体并将其命名为a
  2. 使用a.ParseInputTypeA(...)a.ParseInputTypeB(...)a.ParseInputTypeAV2(...)。你应该明白这个思路。
  3. 运行pkg.MySecretRecipe(&a)来执行你的优化[只要使用指针操作,就没问题]
  4. 类似于步骤2,使用a.PrintOutputTypeA()来输出特定版本的特定结果。
func main() {
    fmt.Println("hello world")
}

在Go中处理这种通用函数签名的需求,可以通过interface{}(Go 1.18+使用any)结合闭包或适配器模式来实现。核心思路是将不同签名的用户函数包装成统一签名的函数,然后通过类型断言在内部处理具体类型。

以下是几种实现方案:

方案1:使用闭包包装函数

package optimizer

type Optimizer struct {
    OptimizeFunc func(interface{}) float64
    CheckFunc    func(interface{}) bool
}

// 用户自定义函数适配器
func NewOptimizer(
    optimizeFunc interface{},
    checkFunc interface{},
) *Optimizer {
    return &Optimizer{
        OptimizeFunc: wrapOptimize(optimizeFunc),
        CheckFunc:    wrapCheck(checkFunc),
    }
}

// 包装优化函数
func wrapOptimize(f interface{}) func(interface{}) float64 {
    switch fn := f.(type) {
    case func(int, int) float64:
        return func(params interface{}) float64 {
            p := params.([2]int)
            return fn(p[0], p[1])
        }
    case func([]int) float64:
        return func(params interface{}) float64 {
            return fn(params.([]int))
        }
    // 添加更多函数签名支持
    default:
        panic("unsupported optimize function signature")
    }
}

// 包装检查函数
func wrapCheck(f interface{}) func(interface{}) bool {
    switch fn := f.(type) {
    case func(int, int, int) bool:
        return func(params interface{}) bool {
            p := params.([3]int)
            return fn(p[0], p[1], p[2])
        }
    case func([]float64, bool, float64) bool:
        return func(params interface{}) bool {
            p := params.([]interface{})
            return fn(p[0].([]float64), p[1].(bool), p[2].(float64))
        }
    // 添加更多函数签名支持
    default:
        panic("unsupported check function signature")
    }
}

方案2:泛型实现(Go 1.18+)

package optimizer

type Optimizer[T any, C any] struct {
    OptimizeFunc func(T) float64
    CheckFunc    func(C) bool
    Params       T
    Conditions   C
}

// 用户使用示例
func ExampleUsage() {
    // 示例1:int参数优化
    opt1 := Optimizer[[2]int, [3]int]{
        OptimizeFunc: func(p [2]int) float64 {
            return float64(p[0]*p[1]) / 2.0
        },
        CheckFunc: func(c [3]int) bool {
            return c[0]+c[1] > c[2]
        },
        Params:     [2]int{10, 20},
        Conditions: [3]int{5, 10, 12},
    }
    
    // 示例2:切片参数优化
    opt2 := Optimizer[[]int, []float64]{
        OptimizeFunc: func(x []int) float64 {
            sum := 0
            for _, v := range x {
                sum += v
            }
            return float64(sum) / float64(len(x))
        },
        CheckFunc: func(x []float64) bool {
            for _, v := range x {
                if v < 0 {
                    return false
                }
            }
            return true
        },
        Params:     []int{1, 2, 3, 4, 5},
        Conditions: []float64{1.1, 2.2, 3.3},
    }
}

方案3:反射实现(最灵活但性能较低)

package optimizer

import (
    "reflect"
)

type UniversalOptimizer struct {
    optimizeFunc reflect.Value
    checkFunc    reflect.Value
}

func NewUniversalOptimizer(optimizeFunc, checkFunc interface{}) *UniversalOptimizer {
    return &UniversalOptimizer{
        optimizeFunc: reflect.ValueOf(optimizeFunc),
        checkFunc:    reflect.ValueOf(checkFunc),
    }
}

func (uo *UniversalOptimizer) RunOptimize(params ...interface{}) float64 {
    in := make([]reflect.Value, len(params))
    for i, param := range params {
        in[i] = reflect.ValueOf(param)
    }
    
    results := uo.optimizeFunc.Call(in)
    return results[0].Float()
}

func (uo *UniversalOptimizer) RunCheck(params ...interface{}) bool {
    in := make([]reflect.Value, len(params))
    for i, param := range params {
        in[i] = reflect.ValueOf(param)
    }
    
    results := uo.checkFunc.Call(in)
    return results[0].Bool()
}

使用示例

// 用户定义不同签名的函数
func optimize1(x, y int) float64 {
    return float64(x*y) / 2.0
}

func check1(x, y, z int) bool {
    return x+y > z
}

func optimize2(x []int) float64 {
    sum := 0
    for _, v := range x {
        sum += v
    }
    return float64(sum)
}

func check2(x []float64, y bool, c float64) bool {
    if !y {
        return false
    }
    for _, v := range x {
        if v > c {
            return false
        }
    }
    return true
}

func main() {
    // 使用方案1
    opt1 := NewOptimizer(optimize1, check1)
    result1 := opt1.OptimizeFunc([2]int{10, 20})
    checkResult1 := opt1.CheckFunc([3]int{5, 10, 12})
    
    // 使用方案3
    opt2 := NewUniversalOptimizer(optimize2, check2)
    result2 := opt2.RunOptimize([]int{1, 2, 3, 4, 5})
    checkResult2 := opt2.RunCheck([]float64{1.1, 2.2, 3.3}, true, 5.0)
}

选择方案的建议:

  1. 如果使用Go 1.18+且函数签名种类有限,推荐方案2(泛型)
  2. 如果需要支持任意签名且性能要求不高,使用方案3(反射)
  3. 方案1在性能和灵活性之间取得平衡,但需要预定义支持的签名

所有方案都避免了在结构体中固定函数签名的问题,允许用户传入任意签名的函数。

回到顶部