Golang中如何为导入的类型实现自定义序列化/反序列化方法

Golang中如何为导入的类型实现自定义序列化/反序列化方法 在我的项目中,有两个Go包,worlddatabase,以及两个Go程序 clientserver

gopath
  - src
    - world (导入 encoding/json)
    - database (导入 world, 导入 dynamodb)
    - client (导入 world, encoding/json)
    - server (导入 world, database, encoding/json)

world 包包含许多 clientserver 共用的枚举器,例如:

const (
    HammerTool ToolType = iota
    TrowelTool
    FrameTool
    PistolTool
    CannonTool
)

由于 encoding/json 用于客户端和服务器之间的通信,world 包为每个枚举器包含了相应的JSON编组器和解组器。

func (toolType ToolType) MarshalText() ([]byte, error) {
    switch toolType {
    case HammerTool:
        return []byte("hammer"), nil
    case TrowelTool:
        return []byte("trowel"), nil
    case FrameTool:
        return []byte("frame"), nil
    case PistolTool:
        return []byte("pistol"), nil
    case CannonTool:
        return []byte("cannon"), nil
    default:
        return nil, errors.New("Unknown tool " + fmt.Sprint(toolType))
    }
}

func (toolType *ToolType) UnmarshalText(text []byte) error {
    switch string(text) {
    case "hammer":
        *toolType = HammerTool
    case "trowel":
        *toolType = TrowelTool
    case "frame":
        *toolType = FrameTool
    case "pistol":
        *toolType = PistolTool
    case "cannon":
        *toolType = CannonTool
    default:
        return errors.New("Unknown tool " + string(text))
    }
    return nil
}

问题出现在 database 包中将枚举器保存到DynamoDB时。DynamoDB有其自己的编组器和解组器:

// 注意:这些方法从未经过测试,因为它们没有被编译。
// 它们可能包含错误,但应该能传达基本思路。
func (toolType ToolType) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
   txt, err := toolType.MarshalText()
   if err != nil {
      return err
   }
   av.S = aws.String(txt)
   return nil
}

func (toolType *ToolType) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    return toolType.UnmarshalText([]byte(*av.S))
}

这些方法是JSON编组器/解组器的简单封装。然而,它们无法被编译进 database 包,因为Go不允许在导入的类型上添加额外的接收器。此外,它们也不能被添加到 world 包中,因为 worldclient 导入,而 client 无法承受由于导入暴露 dynamodb.AttributeValue 类型的DynamoDB包而导致其大小增加数兆字节。毕竟,client 会被编译成 js/wasm 并通过网络下载到网络浏览器……并且根本用不到DynamoDB。

我的问题是:如何为从其他包导入的类型实现自定义的编组器/解组器?

注意事项:

  • 我不想继续将枚举器作为整数编组到DynamoDB,因为那样在添加新的枚举值时会有数据损坏的风险。
  • 最好所有与DynamoDB相关的内容,包括所有DynamoDB特定的编组器,都限制在 database 包内。
  • DynamoDB SDK提供了一个 string 结构体标签,但例如,它会把 TrowelTool 编码为 {S:"1"} 而不是 {S:"trowel"},这与普通的 {N:"1"} 编码方式不同。

更多关于Golang中如何为导入的类型实现自定义序列化/反序列化方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

您意识到,没有什么能阻止您在图形用户界面中以不同于Go代码中的顺序来排列任何内容。

更多关于Golang中如何为导入的类型实现自定义序列化/反序列化方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


虽然这并不十分重要,但还有第三个衡量标准:冗余度。除非绝对必要,否则我不想重复自己(就条目的顺序而言)。

将GUI的顺序与后端绑定,同时确保了最糟糕的紧耦合和低内聚。你需要的是松耦合和高内聚。如果你不熟悉这些术语/概念,请自行搜索了解。

谢谢!我意识到在某些情况下,我或许可以通过在末尾添加条目来解决问题,但在我用作示例的情况下,条目的顺序决定了它们在用户界面中的显示顺序。如果我添加另一个项目,我需要完全灵活地决定将其放置在何处,以便我的用户界面能达到最佳顺序。

你说得对。图形用户界面是用JavaScript实现的,它可以为每种类型定义自己的顺序。但我原本希望能避免那样做。我可以用 _ 在每个条目之间预留一些数字,但话说回来,我原本希望有一个更优雅的解决方案(编辑: 而且那样做会带来一个副作用,即更难验证类型,因为我不能再使用 valid = 0 < x && x <= CannonTool 这样的判断了)。

此外,将枚举值作为字符串存储在DynamoDB中可以使数据库对人类可读。

谢谢!我确实考虑过在 database 包中创建我的结构体层次结构的镜像,为每个枚举器使用特殊类型,就像你演示的那样,然后在顶层进行一次 unsafe 转换。我很确定这会奏效,但它引入了新的问题,比如两个包必须保持同步(或者必须实现一个代码生成器)。不过,这可能是唯一的方法。

我很好奇:如果我为层次结构中的每个结构体以及每个需要自定义编组的类型都创建一个镜像,这会给编译后的二进制文件带来极端的冗余,还是编译器能够对此进行优化?

这通常无法在导入类型上定义自定义编组器,但我发现一个第三方DynamoDB包支持TextMarshaler接口。我认为在我的具体情况下,切换到该包是最优雅的解决方案。

GitHub

guregu/dynamo

Avatar

Go语言的表达性DynamoDB库。通过在GitHub上创建账户来为guregu/dynamo的开发做出贡献。

我不想继续将枚举值以整数的形式序列化到 DynamoDB 中,因为那样的话,在添加新的枚举值时就有可能破坏数据完整性。

为什么这么说呢?只要你只在列表末尾添加新的条目,枚举值就会持续递增并保持唯一性。iota 只会在下一个 const 关键字处重置。

你还可以为它们定义一个 String 函数,这是一种“最佳实践”:

func (t ToolType) String() string {
    return [...]string{"Hammer", "Trowel", "Frame", "Pistol", "Cannon"}[t]
}

然后,将这个数组放在 UnMarshall 能够使用它来将 String 表示形式映射回 index 的地方。

你可以在 database 包中定义另一个 ToolType

package database

import (
    "world"
)

type ToolType world.ToolType

func (toolType ToolType) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
   txt, err := world.ToolType(toolType).MarshalText()
   if err != nil {
      return err
   }
   av.S = aws.String(txt)
   return nil
}

func (toolType *ToolType) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    return ((*world.ToolType)(toolType)).UnmarshalText([]byte(*av.S))
}

然后由 server 来处理 world.ToolTypedatabase.ToolType 之间的转换。

编辑:UnmarshalText 的调用有误。

你不需要以这种方式使用 unsafe 转换。你可以像我上面演示的那样,在必要时将值转换回其 world 包中的对应类型。唯一需要与 database 包保持同步的逻辑是任何与 *dynamodb.AttributeValue 之间进行(反)序列化相关的部分,我认为这正是你问题的基本定义。除了转移问题(例如将 (Un)MarshalDynamoDBAttributeValue 移到 world 包中)或将其通用化之外,我想不出其他简化方法。例如,如果你要序列化的所有类型都实现了 encoding.TextMarshalerencoding.TextUnmarshaler,你可以这样做:

type TextMarshaler interface {
    encoding.TextMarshaler
    encoding.TextUnmarshaler
}

type DBMarshaler struct {
    TextMarshaler
}

func (m DBMarshaler) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    txt, err := m.MarshalText()
    if err != nil {
        return err
    }
    av.S = aws.String(txt)
    return nil
}

func (m DBMarshaler) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    return m.TextUnmarshaler.UnmarshalText([]byte(*av.S))
}

然后在序列化你的 world.ToolType 之前,这样做:

tt := world.HammerTool
av, err := dynamodbattribute.Marshal(DBMarshaler{tt})
// ...
err := dynamodbattribute.Unmarshal(av, DBMarshaler{tt})

在Go中,为导入的类型实现自定义序列化/反序列化方法,可以通过以下几种方式解决:

方法1:使用包装类型(推荐)

database包中创建包装类型,实现DynamoDB的编组器:

// database/tool_type.go
package database

import (
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/aws"
    "world"
)

type ToolTypeWrapper struct {
    world.ToolType
}

func (w ToolTypeWrapper) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    txt, err := w.ToolType.MarshalText()
    if err != nil {
        return err
    }
    av.S = aws.String(string(txt))
    return nil
}

func (w *ToolTypeWrapper) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    if av.S == nil {
        return errors.New("nil string attribute")
    }
    return w.ToolType.UnmarshalText([]byte(*av.S))
}

// 使用示例
func SaveTool(tool world.ToolType) error {
    wrapper := ToolTypeWrapper{tool}
    // 使用wrapper进行DynamoDB操作
}

方法2:使用自定义编组器函数

创建独立的编组器函数,而不是方法:

// database/marshal.go
package database

import (
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/aws"
    "world"
)

func MarshalToolTypeToDynamoDB(tool world.ToolType) (*dynamodb.AttributeValue, error) {
    txt, err := tool.MarshalText()
    if err != nil {
        return nil, err
    }
    return &dynamodb.AttributeValue{
        S: aws.String(string(txt)),
    }, nil
}

func UnmarshalToolTypeFromDynamoDB(av *dynamodb.AttributeValue) (world.ToolType, error) {
    if av.S == nil {
        return 0, errors.New("nil string attribute")
    }
    
    var tool world.ToolType
    err := tool.UnmarshalText([]byte(*av.S))
    return tool, err
}

方法3:使用接口适配器

创建适配器接口来处理不同类型的编组:

// database/dynamodb_adapter.go
package database

import (
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "world"
)

type DynamoDBMarshaler interface {
    MarshalDynamoDB() (*dynamodb.AttributeValue, error)
    UnmarshalDynamoDB(*dynamodb.AttributeValue) error
}

type ToolTypeAdapter struct {
    Value world.ToolType
}

func (a ToolTypeAdapter) MarshalDynamoDB() (*dynamodb.AttributeValue, error) {
    txt, err := a.Value.MarshalText()
    if err != nil {
        return nil, err
    }
    return &dynamodb.AttributeValue{
        S: aws.String(string(txt)),
    }, nil
}

func (a *ToolTypeAdapter) UnmarshalDynamoDB(av *dynamodb.AttributeValue) error {
    if av.S == nil {
        return errors.New("nil string attribute")
    }
    return a.Value.UnmarshalText([]byte(*av.S))
}

方法4:使用结构体标签和自定义编组器(DynamoDB特定)

利用DynamoDB SDK的dynamodbav标签和自定义编组器:

// database/models.go
package database

import (
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
    "world"
)

type ToolRecord struct {
    ToolType string `dynamodbav:"tool_type"`
}

func SaveToolToDynamoDB(tool world.ToolType) error {
    txt, err := tool.MarshalText()
    if err != nil {
        return err
    }
    
    record := ToolRecord{
        ToolType: string(txt),
    }
    
    av, err := dynamodbattribute.MarshalMap(record)
    if err != nil {
        return err
    }
    
    // 使用av进行DynamoDB操作
    return nil
}

func LoadToolFromDynamoDB() (world.ToolType, error) {
    // 从DynamoDB获取数据
    var record ToolRecord
    err := dynamodbattribute.UnmarshalMap(dynamoResult.Item, &record)
    if err != nil {
        return 0, err
    }
    
    var tool world.ToolType
    err = tool.UnmarshalText([]byte(record.ToolType))
    return tool, err
}

方法5:使用泛型包装器(Go 1.18+)

如果使用Go 1.18或更高版本,可以使用泛型:

// database/generic_wrapper.go
package database

import (
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/aws"
)

type TextMarshaler interface {
    MarshalText() ([]byte, error)
    UnmarshalText([]byte) error
}

type DynamoDBWrapper[T TextMarshaler] struct {
    Value T
}

func (w DynamoDBWrapper[T]) MarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    txt, err := w.Value.MarshalText()
    if err != nil {
        return err
    }
    av.S = aws.String(string(txt))
    return nil
}

func (w *DynamoDBWrapper[T]) UnmarshalDynamoDBAttributeValue(av *dynamodb.AttributeValue) error {
    if av.S == nil {
        return errors.New("nil string attribute")
    }
    return w.Value.UnmarshalText([]byte(*av.S))
}

// 使用示例
func SaveToolGeneric(tool world.ToolType) error {
    wrapper := DynamoDBWrapper[world.ToolType]{Value: tool}
    // 使用wrapper进行DynamoDB操作
}

这些方法都能在database包内实现DynamoDB编组器,同时保持world包的纯净,避免引入DynamoDB依赖到client包中。方法1(包装类型)是最直接和兼容性最好的解决方案。

回到顶部