Golang迁移架构问题探讨

Golang迁移架构问题探讨 大家好,

这是我的第一篇帖子,希望这个问题发布在了正确的版块。

我使用 Node.js 开发一个平台已经有一段时间了,该平台包含两个 GraphQL 网关和十几个不同的微服务。架构相当简单:请求到达网关,GraphQL 将请求路由到正确的服务,服务执行一些操作(如创建数据),然后将结果返回给网关,最终返回给客户端。

我决定将微服务从 Node.js 转换为 Go,因为我实在无法再忍受使用 Node.js/JS 进行开发了。我害怕在后端继续编写 JS/Node.js 代码。自从几周前开始接触 Go 以来,我彻底爱上了它,重新燃起了开发的热情。继续使用 Node.js 就像维持一段不健康、功能失调的关系,毫无信任,每天都要面对疯狂和折磨,哈哈。

在当前平台中,每个微服务都可以访问一个标准库,该库包含大约 24 个函数,用于执行创建、获取、删除和验证数据等操作。该库设计支持多语言持久化,我们使用了 Neo4j、MongoDB、PostgreSQL 和 Redis。

主库中的每个函数(例如 create)都被分解为更小的函数(组件),这些组件通过自定义的 pipe 函数进行组合。pipe 函数接收一个 create 组件 数组,执行每个组件,并构建一个“不可变”的请求状态对象,其中包含每个函数执行的结果。某些函数需要前一个函数的结果,并从状态对象中获取。如果某个步骤失败,处理程序会接管请求状态,确保数据库正确回滚。

我希望采用类似的风格,编写一个 Go 库,供所有 Go 服务使用,但我不确定这种架构在 Go 中是否被视为良好实践?我仍然相对新手。我会有用于创建、获取、软删除、硬删除、列表、更新等的函数,以及每个步骤的函数。目前,每个步骤(函数/组件)在父目录(如 create)中有自己的文件。

我希望避免使用 a(b(c(d()))) 风格的函数组合,而是倾向于遍历函数切片。

也许这种思维方式在 Go 中是错误的,有更好的方法?

无论如何,我希望得到一些建议,如何将当前构建的内容迁移到 Go。

谢谢!


更多关于Golang迁移架构问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

感谢您的回复。我做了大量研究后得出结论,我在 Node.js 中的流水线设计可能不适合 Go。

更多关于Golang迁移架构问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


嗨,Dan,欢迎来到论坛!

我想说的是,我想象你打算做的事情看起来是合理的,但你的问题非常抽象,所以我无法准确判断你想要实现的方法是否可行。

如果你遇到问题并提供一些示例代码,我可以给你更具体的回答。但在那之前,我的建议是放手尝试吧!

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

我本来已经写了一大半回复,但我想说肖恩是对的:描述有些模糊,我们需要更多信息才能帮助你。

无论如何,先别急着放弃。你看起来非常渴望抛弃 Node.js/JavaScript,也许 Go 并不是答案,而 Rust、Haskell、C++、Java 或其他语言更适合你的想法。但如果你能提供一些代码展示你的某个服务是如何工作的(以及你尝试如何将其移植到 Go),我们或许可以在这方面帮助你。

你好 @iegomez

抱歉,明天我会提供更多细节。出于多种原因,我非常渴望放弃 Node,并且我认为 Go 是正确的语言,只是它与 JS/Node 非常不同,需要一种不同于 Node 中使用的动态架构的架构。

例如,我有一个服务(CRUD),其中有一个名为 /create 的路由。该创建路由用于在 Neo4j/MongoDB 中创建“事物”。一个事物可以是人、汽车、狗、赛道等。每个事物可以有不同的模式。

当请求从 GraphQL 传递到创建服务处理程序时,服务首先验证事物类型,然后确保它是活跃的。接着,服务会加载一个包含该事物类型的归约函数和 Neo4j Cypher 字符串的对象。归约函数接收请求对象,并返回一个字段更少或更多的新对象。在创建数据的每个步骤中都有一个归约函数。一个归约函数用于减少请求参数以检查正在使用的属性,另一个用于进入 Neo4j 的字段,还有一个用于 Neo4j 插入的结果,这些结果与某些请求参数结合后进入 MongoDB 文档,依此类推。处理程序可以创建单个事物,也可以处理包含数百个或更多事物的数组。

只要给定有效的事物类型并且系统支持它,创建处理程序就可以创建任何事物类型。向系统添加事物类型就是添加事物类型配置、归约函数、Cypher 查询和 MongoDB 模式。添加额外的事物类型非常快速和高效。

删除、更新、读取等都以类似的方式工作。

在 Go 中,根据我所学的,你不会这样做,而是可能会为每个事物类型创建一个处理程序或函数,这可能会带来更多的复杂性。更多的代码和重复代码,以及更多的组件。

到目前为止,我对 Go 唯一的担忧是,它可能会变成类型和错误处理的“模拟器”,只夹杂着一点业务逻辑。

希望这说得通。😊

该睡觉了。

谢谢!

在 Go 中实现类似的管道式架构是完全可行的,并且可以保持代码的清晰和可维护性。以下是一个示例实现,展示如何通过函数切片和状态对象来构建类似的流程:

package main

import (
	"context"
	"errors"
	"fmt"
)

// State 表示请求状态对象
type State struct {
	Data   map[string]interface{}
	Errors []error
}

// Component 是处理组件的类型
type Component func(ctx context.Context, state *State) error

// Pipe 执行组件管道
func Pipe(ctx context.Context, components ...Component) (*State, error) {
	state := &State{
		Data: make(map[string]interface{}),
	}
	
	for _, component := range components {
		if err := component(ctx, state); err != nil {
			state.Errors = append(state.Errors, err)
			return state, err
		}
	}
	return state, nil
}

// 示例组件
func ValidateInput(ctx context.Context, state *State) error {
	if state.Data["input"] == nil {
		return errors.New("input is required")
	}
	return nil
}

func CreateInNeo4j(ctx context.Context, state *State) error {
	fmt.Println("Creating in Neo4j:", state.Data["input"])
	state.Data["neo4jId"] = "12345"
	return nil
}

func CreateInMongo(ctx context.Context, state *State) error {
	fmt.Println("Creating in MongoDB:", state.Data["neo4jId"])
	state.Data["mongoId"] = "67890"
	return nil
}

func UpdateRedis(ctx context.Context, state *State) error {
	fmt.Println("Updating Redis with:", state.Data["mongoId"])
	return nil
}

func main() {
	ctx := context.Background()
	
	components := []Component{
		ValidateInput,
		CreateInNeo4j,
		CreateInMongo,
		UpdateRedis,
	}
	
	state := &State{
		Data: map[string]interface{}{
			"input": "test data",
		},
	}
	
	resultState, err := Pipe(ctx, components...)
	if err != nil {
		fmt.Printf("Pipeline failed: %v\n", err)
		fmt.Printf("Errors: %v\n", resultState.Errors)
		return
	}
	
	fmt.Printf("Final state: %+v\n", resultState.Data)
}

对于数据库操作和事务管理,可以这样扩展:

type DBTransaction interface {
	Begin(ctx context.Context) error
	Commit() error
	Rollback() error
}

type TransactionManager struct {
	transactions []DBTransaction
}

func (tm *TransactionManager) ExecuteInTransaction(ctx context.Context, components ...Component) (*State, error) {
	// 开始所有事务
	for _, tx := range tm.transactions {
		if err := tx.Begin(ctx); err != nil {
			return nil, err
		}
	}
	
	state, err := Pipe(ctx, components...)
	if err != nil {
		// 回滚所有事务
		for _, tx := range tm.transactions {
			tx.Rollback()
		}
		return state, err
	}
	
	// 提交所有事务
	for _, tx := range tm.transactions {
		if err := tx.Commit(); err != nil {
			return state, err
		}
	}
	
	return state, nil
}

在微服务中使用的示例:

package userservice

import (
	"context"
	
	"github.com/your-org/common-library"
)

func CreateUser(ctx context.Context, input map[string]interface{}) error {
	components := []common.Component{
		common.ValidateUserInput,
		common.CreateUserInPostgreSQL,
		common.CreateUserProfileInMongo,
		common.UpdateUserCacheInRedis,
	}
	
	state := &common.State{
		Data: input,
	}
	
	_, err := common.Pipe(ctx, components...)
	return err
}

这种架构在 Go 中的优势:

  • 明确的错误处理流程
  • 易于测试单个组件
  • 可组合的业务逻辑
  • 清晰的数据流追踪

可以通过接口进一步抽象数据库操作,保持与 Node.js 版本类似的多数据库支持能力。

回到顶部