从数据库变量/值动态运行函数的Golang实现方案

从数据库变量/值动态运行函数的Golang实现方案 你好,

我是Go语言的新手,如果说错了什么或写错了什么,请见谅 🙂

我正在尝试将函数作为作业运行,并将函数名存储在数据库中,然后尝试将它们作为Cronjob运行,但在需要执行时遇到了问题。

所以我尝试将它们映射起来:

var funcs = map[string]func() {"countRegistrations":countRegistrations}

然后用以下方式执行:

funcs["\""+name+"\""]()

其中 name 是从数据库中检索的,但这会引发内存错误恐慌。

我还发现这不够灵活,因为我可以添加新作业(数据库中的函数名),但对于映射,我必须重启数据库。

有没有办法调用存储在变量 name 中的函数名所对应的函数?

谢谢


更多关于从数据库变量/值动态运行函数的Golang实现方案的实战教程也可以访问 https://www.itying.com/category-94-b0.html

16 回复

让我查一下,谢谢

更多关于从数据库变量/值动态运行函数的Golang实现方案的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢大家的帮助,我决定保留为映射结构。

我认为你需要使用反射来通过函数名调用你的函数

您需要修改并部署代码以修改映射。

func main() {
    fmt.Println("hello world")
}

是否有一种方法也能将 "hello" 参数化,这样我就不必在代码中硬编码密钥,并且在每次添加新记录时也无需重新启动邮件服务?

var funcs = map[string]func() {"funcA":funcA}

参见 Go Playground - The Go Programming Language

你是想将Go对象(函数)追加到映射中,还是它已经存在于数据库中,你只是使用名称来调用该对象?

映射的追加操作大致如下:

m := make(map[string], 0)
m[key] = func

嘿,谢谢你的建议。我想我之前试过不加引号,结果返回了 nil,但这可能是因为我之前遇到的数据库获取问题导致的。我会再试试。

至于更新映射的工作,这确实是个好主意。我已经有从数据库获取数据的代码了,忘了我也能更新映射 😉

非常感谢。

你好。你确定键中需要那些额外的引号吗?最好总是检查键是否存在,例如:

fn, ok := funcs[name]
if ok {
    fn()
}

另外,你或许可以添加一些任务来更新 funcs 映射,以便从数据库重新加载新值。

确实,现在不加引号也能正常工作了 😊 很好。

但我不确定如何将它 append 到 map 中,目前看起来是这样的:

var funcs = map[string]func() {"funcA":funcA}

所以我又不确定该如何调用 append 了,还是因为引号的问题 😊

我不太确定我理解你的问题。你是说你想向你的函数传递一个参数吗?我在这里为你修改了Edward发布的示例:

Go Playground - The Go Programming Language

糟糕的代码

var funcs = map[string]func() {"countRegistrations":countRegistrations}

funcs["\""+name+"\""]()

良好的代码

var funcs = map[string]func() {"countRegistrations":countRegistrations}

yourFuncName := "countRegistrations"

fn, ok := funcs[yourFuncName]
if !ok {
    panic(fmt.Errorf("func not found: %s", yourFuncName)
}
fn()

我的想法是创建一个任务,从数据库中获取那些尚未更新 jobId 的新记录。funName 字段只是一个字符串(varchar),代表我希望从那个主任务中运行的函数名称(作为另一个任务,然后让该函数作为独立任务运行)。这样我就可以动态地添加更多任务。

我最初尝试直接调用 funName,但遇到了:

./main.go:137:4: funName (variable of type string) is not used

这引导我创建了那个 funcs 映射,它将字面字符串映射到函数名。现在,当我在该映射中使用硬编码的字符串和名称时,它可以工作了。

抱歉,之前说得有些混乱;)

我的目标是拥有一个字符串 = 需要作为任务运行/执行的函数名称。

因此,以你的代码片段为例,第13行的键需要从数据库中动态添加。 所以我在 main.go 中有一个任务(cronjob),它会获取所有待处理的任务,然后,如果其中一条记录在数据库中没有更新 job.ID(),我就想运行它,而 name 字段就是将要执行的函数名称。

正如第一个回复所说,我通过使用一个 map{字符串名称: 函数名称} 成功运行了它,所以我在想是否可以向该映射中添加新的键: 函数名,但我读到这样做不太安全,因为那样会暴露代码注入的风险。

确实如此。或者,你也可以通过调用脚本语言来执行任意代码。例如,如果你想执行任意的 JavaScript,有几种方法可以实现:

GitHub - rogchap/v8go: Execute JavaScript from Go

GitHub - dop251/goja: ECMAScript/JavaScript engine in pure Go

GitHub - tidwall/go-node: Run Javascript in Go using Node.js

如果你选择这条路线,你可以直接将一段 JavaScript 代码存储在数据库中,而不是存储函数名。等等。

在Go中从数据库动态调用函数是可行的,但需要一些技巧。这里提供几种实现方案:

方案1:使用函数映射(推荐)

这是最安全的方式,可以通过注册机制实现动态添加:

package main

import (
    "database/sql"
    "fmt"
    "sync"
)

// 作业函数类型
type JobFunc func() error

// 全局作业注册表
var (
    jobRegistry = make(map[string]JobFunc)
    registryMutex sync.RWMutex
)

// 注册作业
func RegisterJob(name string, job JobFunc) {
    registryMutex.Lock()
    defer registryMutex.Unlock()
    jobRegistry[name] = job
}

// 执行作业
func ExecuteJob(name string) error {
    registryMutex.RLock()
    job, exists := jobRegistry[name]
    registryMutex.RUnlock()
    
    if !exists {
        return fmt.Errorf("job %s not found", name)
    }
    
    return job()
}

// 示例作业函数
func countRegistrations() error {
    fmt.Println("Counting registrations...")
    // 业务逻辑
    return nil
}

func sendNotifications() error {
    fmt.Println("Sending notifications...")
    // 业务逻辑
    return nil
}

// 初始化时注册作业
func init() {
    RegisterJob("countRegistrations", countRegistrations)
    RegisterJob("sendNotifications", sendNotifications)
}

// 从数据库获取并执行作业
func runJobFromDB(db *sql.DB, jobID string) error {
    var jobName string
    err := db.QueryRow("SELECT function_name FROM jobs WHERE id = ?", jobID).Scan(&jobName)
    if err != nil {
        return err
    }
    
    return ExecuteJob(jobName)
}

方案2:使用反射(更灵活但性能较低)

如果需要完全动态的调用,可以使用反射:

package main

import (
    "database/sql"
    "fmt"
    "reflect"
)

// 作业结构体
type JobManager struct{}

func (jm *JobManager) CountRegistrations() error {
    fmt.Println("Counting registrations via reflection...")
    return nil
}

func (jm *JobManager) SendNotifications() error {
    fmt.Println("Sending notifications via reflection...")
    return nil
}

// 使用反射动态调用方法
func CallJobByName(jobManager interface{}, methodName string) error {
    val := reflect.ValueOf(jobManager)
    method := val.MethodByName(methodName)
    
    if !method.IsValid() {
        return fmt.Errorf("method %s not found", methodName)
    }
    
    // 调用方法
    results := method.Call(nil)
    
    // 检查错误
    if len(results) > 0 {
        if err, ok := results[0].Interface().(error); ok {
            return err
        }
    }
    
    return nil
}

// 从数据库执行
func runReflectiveJob(db *sql.DB, jobID string) error {
    var jobName string
    err := db.QueryRow("SELECT function_name FROM jobs WHERE id = ?", jobID).Scan(&jobName)
    if err != nil {
        return err
    }
    
    jm := &JobManager{}
    return CallJobByName(jm, jobName)
}

方案3:结合数据库的动态注册

如果需要从数据库动态添加新作业而不重启:

package main

import (
    "database/sql"
    "fmt"
    "plugin"
    "path/filepath"
)

// 动态插件系统
type PluginJob interface {
    Execute() error
    Name() string
}

var pluginJobs = make(map[string]PluginJob)

// 从插件加载作业
func LoadJobFromPlugin(pluginPath, jobName string) error {
    p, err := plugin.Open(pluginPath)
    if err != nil {
        return err
    }
    
    sym, err := p.Lookup("NewJob")
    if err != nil {
        return err
    }
    
    newJobFunc, ok := sym.(func() PluginJob)
    if !ok {
        return fmt.Errorf("invalid plugin signature")
    }
    
    job := newJobFunc()
    pluginJobs[job.Name()] = job
    return nil
}

// 从数据库加载并执行插件作业
func runPluginJobFromDB(db *sql.DB, jobID string) error {
    var jobName, pluginPath string
    err := db.QueryRow(
        "SELECT job_name, plugin_path FROM plugin_jobs WHERE id = ?", 
        jobID,
    ).Scan(&jobName, &pluginPath)
    if err != nil {
        return err
    }
    
    // 如果插件未加载,先加载
    if _, exists := pluginJobs[jobName]; !exists {
        absPath, _ := filepath.Abs(pluginPath)
        if err := LoadJobFromPlugin(absPath, jobName); err != nil {
            return err
        }
    }
    
    job := pluginJobs[jobName]
    return job.Execute()
}

方案4:使用脚本引擎(最灵活)

对于高度动态的场景,可以嵌入脚本引擎:

package main

import (
    "database/sql"
    "github.com/dop251/goja"
    "fmt"
)

// 使用Goja JavaScript引擎执行动态脚本
func executeScriptJob(db *sql.DB, jobID string) error {
    var scriptContent string
    err := db.QueryRow("SELECT script FROM script_jobs WHERE id = ?", jobID).Scan(&scriptContent)
    if err != nil {
        return err
    }
    
    vm := goja.New()
    
    // 注册Go函数到JavaScript环境
    vm.Set("countRegistrations", func(call goja.FunctionCall) goja.Value {
        fmt.Println("Called from JavaScript")
        // 调用实际的Go函数
        return nil
    })
    
    // 执行脚本
    _, err = vm.RunString(scriptContent)
    return err
}

针对你的问题的直接修复

你遇到的panic可能是因为数据库返回的字符串包含引号或空格:

func safeExecuteJob(name string) error {
    // 清理字符串
    jobName := strings.TrimSpace(name)
    jobName = strings.Trim(jobName, "\"")
    
    // 检查映射
    if jobFunc, exists := funcs[jobName]; exists {
        jobFunc()
        return nil
    }
    
    return fmt.Errorf("job %s not found", jobName)
}

推荐使用方案1的函数映射方式,它提供了良好的类型安全性和性能,同时通过注册机制支持动态扩展。对于需要热更新的场景,可以结合方案3的插件系统。

回到顶部