GO [ 数据格式 ]

数据格式

在当前互联网时代,程序很少只在自己的进程中处理数据。网页接口要把数据返回给浏览器,配置中心要把配置发送给服务,日志系统要把事件交给其他程序,RPC 服务还要在不同语言之间传递请求和响应。

这些场景有一个共同问题:内存中的 Go 对象,如何变成可以保存、传输、再恢复的数据?

这个过程叫做序列化 。把一段已经保存或接收到的数据恢复成 Go 对象,叫做反序列化。

目前最常见的、独立于具体编程语言的数据格式主要有 XML、YAML、JSON 和 Protocol Buffers。它们都能表达结构化数据,但设计目标不一样:XML 更强调标签和兼容性,YAML 更适合人类编写配置,JSON 更适合 Web 数据交换,Protocol Buffers 则更适合高性能的服务间通信。

下面先做一个横向比较。表格里的"快"和"慢"是工程上的相对印象,实际结果还会受到数据大小、字段数量、实现版本、网络和磁盘等因素影响,不能把它当成固定的 benchmark 结论。

名称 XML YAML JSON Protocol Buffers
数据结构 复杂 较简单 简单 较复杂
保存方式 文本 文本 文本 二进制
保存大小 大 中 中 小
解析效率 慢 中 中 快
语言支持 非常多 多 很多 较多
开发难度 繁琐 较简单 简单 需要代码生成
学习成本 低 低 低 中
适用范围 数据交换、传统系统 配置文件 Web 数据交换 RPC、内部服务通信

简单的选择建议如下:

  • 对外提供 REST API,优先选择 JSON;
  • 写给人修改的配置文件,优先选择 YAML;
  • 对接已经使用 XML 的系统时,使用 XML;
  • 服务之间需要高吞吐、低带宽和严格协议时,使用 Protocol Buffers。

序列化和反序列化

可以把数据处理过程理解成下面这样:

复制代码
Go 结构体  --序列化-->  XML/YAML/JSON/Protobuf 字节
Go 结构体  <--反序列化--  XML/YAML/JSON/Protobuf 字节

在 Go 中,如果想要让结构体参与序列化和反序列化,字段通常必须是对外暴露的字段,也就是字段名首字母大写。小写字段属于包内私有字段,标准库和大多数第三方编码库不会直接访问它们。

复制代码
type Person struct {
	Name string `json:"name"`
}

这里的 Name 可以被编码;如果写成 name,编码器会忽略它。反射是这些库实现自动编解码的基础,字段是否导出是反射能够访问到它的前提。

XML

XML 的全称是 eXtensible Markup Language,它使用开始标签、结束标签和属性描述层级数据。XML 起源较早,曾经广泛用于网络传输、数据交换、配置文件和文档存储。

XML 的优点是格式明确、扩展方式成熟、几乎所有语言都有解析库;缺点也很明显:标签会重复占用空间,文本内容比较冗长,复杂 XML 的解析和维护成本比较高。

Go 官方标准库提供了 encoding/xml 包,不需要额外安装依赖。

XML 的主要 API

复制代码
func Marshal(v any) ([]byte, error)

func MarshalIndent(v any, prefix, indent string) ([]byte, error)

func Unmarshal(data []byte, v any) error
  • Marshal:把 Go 值编码成 XML;
  • MarshalIndent:编码 XML,并为输出增加缩进;
  • Unmarshal:把 XML 字节解析到目标变量中。

encoding/xml 还提供了 Encoder 和 Decoder。当 XML 很大,不能一次性读入内存时,可以使用流式 API:Encoder.Encode 负责写出,Decoder.Decode 负责逐个读取。

定义结构体

复制代码
package main

import (
	"encoding/xml"
	"fmt"
)

type Person struct {
	UserID   string `xml:"id"`
	Username string `xml:"name"`
	Age      int    `xml:"age"`
	Address  string `xml:"address"`
}

结构体标签决定字段对应的 XML 节点名称。如果不写标签,encoding/xml 默认会使用字段名。

XML 序列化

复制代码
func main() {
	person := Person{
		UserID:   "120",
		Username: "jack",
		Age:      18,
		Address:  "usa",
	}

	data, err := xml.MarshalIndent(person, "", "\t")
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(data))
}

输出:

复制代码
<Person>
	<id>120</id>
	<name>jack</name>
	<age>18</age>
	<address>usa</address>
</Person>

根节点默认使用结构体名称 Person。如果希望使用其他根节点,可以给结构体增加 XMLName 字段:

复制代码
type Person struct {
	XMLName  xml.Name `xml:"person"`
	UserID   string   `xml:"id"`
	Username string   `xml:"name"`
	Age      int      `xml:"age"`
}

再次序列化时,根节点会变成小写的 <person>。

XML 反序列化

复制代码
func main() {
	xmlStr := `
<Person>
	<id>120</id>
	<name>jack</name>
	<age>18</age>
	<address>usa</address>
</Person>`

	var person Person
	if err := xml.Unmarshal([]byte(xmlStr), &person); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", person)
}

输出:

复制代码
{UserID:120 Username:jack Age:18 Address:usa}

注意,反序列化的第二个参数必须传指针。编码器需要把解析结果写回原变量,如果传入普通值,函数无法修改调用方的数据。

XML 属性、嵌套节点和重复节点

XML 不只有普通节点,还经常使用属性、嵌套节点和重复节点。结构体标签可以表达这些情况:

复制代码
type Order struct {
	XMLName xml.Name `xml:"order"`
	ID      string   `xml:"id,attr"`
	User    Person   `xml:"user"`
	Items   []Item   `xml:"items>item"`
}

type Item struct {
	Name  string `xml:"name"`
	Count int    `xml:"count"`
}

对应的 XML 可以写成:

复制代码
<order id="A1001">
	<user>
		<id>120</id>
		<name>jack</name>
	</user>
	<items>
		<item><name>keyboard</name><count>1</count></item>
		<item><name>mouse</name><count>2</count></item>
	</items>
</order>

xml:",attr" 表示 XML 属性;xml:"items>item" 表示先进入 items 节点,再读取其中的多个 item 节点。由此可以看出,XML 的表达能力很强,但标签路径也更容易变得复杂。

如果 XML 结构非常复杂,或者需要保留注释、命名空间和任意层级,除了标准库,还可以考虑使用 etree 等第三方库。不过,简单的数据交换优先使用标准库,依赖越少,维护成本越低。

YAML

YAML 的全称是 YAML Ain't Markup Language。它主要使用缩进表示层级,语法接近自然配置文件,因此经常用于应用配置、CI 配置、容器编排和开发工具配置。

YAML 的优点是可读性好,缺点是缩进有语义,空格数量、列表位置和字符串类型都可能影响解析结果。Go 官方标准库没有提供 YAML 包,需要使用第三方库。

目前 Go 项目中常见的选择是 gopkg.in/yaml.v3。

安装 YAML 依赖

复制代码
go get gopkg.in/yaml.v3

导入路径如下:

复制代码
import "gopkg.in/yaml.v3"

YAML 的主要 API

复制代码
func Marshal(in interface{}) (out []byte, err error)

func Unmarshal(in []byte, out interface{}) (err error)
  • yaml.Marshal:把 Go 值序列化为 YAML;
  • yaml.Unmarshal:把 YAML 内容反序列化到目标变量。

库还提供 Encoder、Decoder 和 Node。普通配置文件使用 Marshal、Unmarshal 就够了;需要处理多文档、保留节点信息或做更严格校验时,再使用这些高级 API。

定义配置结构体

复制代码
package main

import (
	"fmt"

	"gopkg.in/yaml.v3"
)

type Config struct {
	Database string `yaml:"database"`
	URL      string `yaml:"url"`
	Port     int    `yaml:"port"`
	Username string `yaml:"username"`
	Password string `yaml:"password"`
}

YAML 标签和 XML、JSON 标签的作用类似,用于把 Go 字段映射到 YAML 键名。

YAML 配置文件

config.yml:

复制代码
database: mysql
url: 127.0.0.1
port: 3306
username: root
password: "123456"

YAML 序列化

复制代码
func main() {
	config := Config{
		Database: "oracle",
		URL:      "localhost",
		Port:     3326,
		Username: "root",
		Password: "123456",
	}

	out, err := yaml.Marshal(config)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(out))
}

输出:

复制代码
database: oracle
url: localhost
port: 3326
username: root
password: "123456"

YAML 本身就是面向人类阅读的文本格式,通常不需要像 JSON 那样额外调用"格式化序列化"函数。

YAML 反序列化

复制代码
package main

import (
	"fmt"
	"os"

	"gopkg.in/yaml.v3"
)

func main() {
	data, err := os.ReadFile("./config.yml")
	if err != nil {
		fmt.Println(err)
		return
	}

	var config Config
	if err := yaml.Unmarshal(data, &config); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", config)
}

输出:

复制代码
{Database:mysql URL:127.0.0.1 Port:3306 Username:root Password:123456}

这里仍然要传 &config,因为 Unmarshal 需要修改 config 的字段。

严格检查 YAML 字段

默认情况下,YAML 中出现结构体没有定义的字段时,很多场景不会直接报错。配置文件一旦写错键名,程序可能悄悄使用零值,这通常很难排查。

可以使用 KnownFields(true) 让解码器遇到未知字段时报告错误:

复制代码
func decodeConfig(data []byte) (Config, error) {
	var config Config
	decoder := yaml.NewDecoder(bytes.NewReader(data))
	decoder.KnownFields(true)

	if err := decoder.Decode(&config); err != nil {
		return Config{}, err
	}
	return config, nil
}

完整代码需要额外导入:

复制代码
import "bytes"

对于生产环境中的配置文件,严格字段检查通常比"遇到未知字段直接忽略"更容易发现问题。

JSON

JSON 的全称是 JavaScript Object Notation。它是一种轻量级文本数据格式,能够表示对象、数组、字符串、数字、布尔值和 null。

JSON 在 RESTful 接口、浏览器通信、消息队列和日志系统中非常常见。它比 XML 更简洁,浏览器和几乎所有后端语言都能直接处理,因此成为 Web 领域最主流的数据交换格式之一。

Go 的标准库 encoding/json 提供了 JSON 编解码能力,不需要安装第三方依赖。

从 Go 1.27 开始,标准库还提供了 encoding/json/v2 和 encoding/json/jsontext。encoding/json 的常用 API 仍然继续支持,本文使用 encoding/json,这样示例可以兼容大量已有项目;如果新项目明确要求更严格的 JSON 语义,可以再阅读官方的 v2 文档。

JSON 的主要 API

复制代码
func Marshal(v any) ([]byte, error)

func MarshalIndent(v any, prefix, indent string) ([]byte, error)

func Unmarshal(data []byte, v any) error
  • Marshal:把 Go 对象编码成紧凑的 JSON;
  • MarshalIndent:把 Go 对象编码成带缩进、方便阅读的 JSON;
  • Unmarshal:把 JSON 字节解析到 Go 对象。

对于文件或网络流,还可以使用:

复制代码
func NewEncoder(w io.Writer) *Encoder

func NewDecoder(r io.Reader) *Decoder

定义结构体

复制代码
package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	UserID   string `json:"id"`
	Username string `json:"name"`
	Age      int    `json:"age"`
	Address  string `json:"address"`
}

如果不写 JSON 标签,默认输出的 key 会使用字段名,例如 UserID。在接口设计中,通常会显式写出小写的 JSON key,这样可以让协议保持稳定,也能避免 Go 命名和前端命名风格不一致。

JSON 序列化

复制代码
func main() {
	person := Person{
		UserID:   "120",
		Username: "jack",
		Age:      18,
		Address:  "usa",
	}

	data, err := json.Marshal(person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(data))
}

输出:

复制代码
{"id":"120","name":"jack","age":18,"address":"usa"}

默认输出没有多余空格,这是为了减少网络传输和文件保存所占用的空间。

JSON 缩进

调试、日志和配置文件经常需要人类阅读,这时可以使用 MarshalIndent:

复制代码
func main() {
	person := Person{
		UserID:   "1202",
		Username: "jack",
		Age:      19,
		Address:  "USA",
	}

	data, err := json.MarshalIndent(person, "", "\t")
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(data))
}

输出:

复制代码
{
	"id": "1202",
	"name": "jack",
	"age": 19,
	"address": "USA"
}

第一个参数 prefix 会添加到每一行的开头,第二个参数 indent 表示每一层使用什么缩进。常见写法是 "" 配合 " " 或 "\t"。

JSON 反序列化

复制代码
func main() {
	jsonStr := `{"id":"120","name":"jack","age":18,"address":"usa"}`

	var person Person
	if err := json.Unmarshal([]byte(jsonStr), &person); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", person)
}

输出:

复制代码
{UserID:120 Username:jack Age:18 Address:usa}

反序列化时,JSON key 会优先按照结构体标签匹配;没有标签时,再按照字段名匹配。JSON 的字段名通常不要求和 Go 字段大小写完全一致,但在接口协议中仍然建议明确写标签,避免依赖宽松匹配规则。

omitempty 忽略空字段

结构体标签还可以增加选项。omitempty 表示字段为零值时不输出:

复制代码
type User struct {
	Name   string `json:"name"`
	Age    int    `json:"age,omitempty"`
	Email  string `json:"email,omitempty"`
	Admin  bool   `json:"admin,omitempty"`
}

func main() {
	user := User{Name: "jack"}
	data, _ := json.Marshal(user)
	fmt.Println(string(data))
}

输出:

复制代码
{"name":"jack"}

对于 int,零值是 0;对于 string,零值是空字符串;对于指针、切片、map 和接口,nil 也属于空值。需要注意的是,omitempty 会把"没有填写"和"明确填写了零值"合并成一种状态。如果业务必须区分这两种情况,可以使用指针或自定义类型。

使用 Decoder 严格检查未知字段

默认的 json.Unmarshal 会忽略 JSON 中结构体没有声明的字段:

复制代码
func decodePerson(data []byte) (Person, error) {
	var person Person
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()

	if err := decoder.Decode(&person); err != nil {
		return Person{}, err
	}
	return person, nil
}

上面的代码需要额外导入:

复制代码
import "bytes"

当 JSON 是外部请求时,DisallowUnknownFields 可以帮助我们更早发现客户端拼写错误;但如果协议需要向后兼容、允许客户端携带未来字段,就不应该盲目打开这个选项。

JSON 数字的注意事项

当 JSON 被解码到 map[string]any 时,数字默认会变成 float64:

复制代码
func main() {
	var value map[string]any
	_ = json.Unmarshal([]byte(`{"id":9007199254740993}`), &value)
	fmt.Printf("%T\n", value["id"])
}

输出类型是:

复制代码
float64

对于很大的整数,直接使用 float64 可能丢失精度。可以让 Decoder 使用 json.Number:

复制代码
func main() {
	decoder := json.NewDecoder(strings.NewReader(`{"id":9007199254740993}`))
	decoder.UseNumber()

	var value map[string]any
	if err := decoder.Decode(&value); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%T %v\n", value["id"], value["id"])
}

需要额外导入:

复制代码
import "strings"

如果知道数据结构,优先反序列化到明确的结构体;只有在结构不固定时,才使用 map[string]any。这条经验比记住某一个 API 更重要。

Protocol Buffers

Protocol Buffers,通常简称 Protobuf,是 Google 开源的、语言中立、平台中立、可扩展的结构化数据序列化机制。它使用 .proto 文件描述数据结构,再通过 protoc 编译器生成各语言的代码。

和 XML、YAML、JSON 相比,Protobuf 的数据通常更小,编码和解析速度也更快,因此常用于 RPC、微服务内部通信、消息队列和高频数据传输。

Protobuf 的代价是需要维护协议文件并执行代码生成,生成后的二进制内容也不适合直接用文本编辑器查看。它更适合"机器之间长期、稳定地交换数据",不适合拿来写普通配置文件。

安装 Go 的 Protobuf 工具

先安装 protoc 编译器。不同操作系统的安装方式可以参考 Protocol Buffers 官方安装文档。

然后安装 Go 代码生成插件:

复制代码
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

确保 protoc-gen-go 所在的目录已经加入 PATH。官方 Go 教程使用的是 google.golang.org/protobuf,老项目中经常能看到的 github.com/golang/protobuf/proto 属于旧 API,新项目优先使用官方新模块。

编写 .proto 文件

在 Go module 根目录下创建 personpb/person.proto:

复制代码
syntax = "proto3";

package person;

option go_package = "example.com/dataformat/personpb";

message Person {
  string name = 1;
  int32 age = 2;
  repeated string emails = 3;
}

这里有几个关键点:

  • syntax = "proto3" 表示使用 proto3 语法;
  • message Person 定义一个消息结构;
  • name = 1 中的数字是字段编号,不是数组下标;
  • repeated 表示一个可以包含多个元素的字段;
  • go_package 告诉生成器应该使用哪个 Go import path。

字段编号一旦发布就应该长期保留。删除字段后不要把旧编号立即分配给新字段,否则旧数据在新代码中可能被解释成完全不同的含义。

生成 Go 文件

在 Go module 根目录执行:

复制代码
protoc --proto_path=. --go_out=. --go_opt=paths=source_relative personpb/person.proto

执行成功后,会生成 personpb/person.pb.go。这个文件不应该手工修改,协议发生变化时重新运行代码生成命令即可。

Protobuf 序列化和反序列化

假设生成的 Go 包路径是 example.com/dataformat/personpb,可以这样使用:

复制代码
package main

import (
	"fmt"

	"google.golang.org/protobuf/proto"
	pb "example.com/dataformat/personpb"
)

func main() {
	person := &pb.Person{
		Name: "wyh",
		Age:  12,
	}

	data, err := proto.Marshal(person)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("proto buffer len:", len(data), "bytes:", data)

	decoded := &pb.Person{}
	if err := proto.Unmarshal(data, decoded); err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(decoded.GetName())
	fmt.Println(decoded.GetAge())
	fmt.Println(decoded.GetEmails())
}

如果只填写 name 和 age,编码结果类似下面这样:

复制代码
proto buffer len: 7 bytes: [10 3 119 121 104 16 12]
wyh
12
[]

Protobuf 使用二进制 wire format 保存字段和值,所以输出的 bytes 不适合直接阅读。调试时可以使用生成结构体的 getter,也可以使用 protojson 把消息转换成 JSON:

复制代码
import (
	"fmt"

	"google.golang.org/protobuf/encoding/protojson"
	pb "example.com/dataformat/personpb"
)

func printJSON(person *pb.Person) {
	text, err := protojson.Marshal(person)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(text))
}

Protobuf 的兼容性规则

Protobuf 的强项不只是"二进制更小",更重要的是它为协议演进提供了明确规则:

  1. 新增字段时,旧程序会忽略自己不认识的字段;
  2. 旧数据被新程序读取时,新字段会使用默认值;
  3. 已经使用过的字段编号不能重复分配;
  4. 删除字段后,可以使用 reserved 保留字段名和编号;
  5. 需要改变字段类型时,不能只看 Go 类型是否能编译,还要考虑 wire format 是否兼容。

例如:

复制代码
message Person {
  reserved 4;
  reserved "old_phone";

  string name = 1;
  int32 age = 2;
  repeated string emails = 3;
}

reserved 可以防止未来的开发者误用已经删除的编号或字段名。

四种格式如何选择

如果只是学习 API,可以把同一个 Person 结构分别编码成四种格式;如果是设计真实项目,则要先看数据的使用方式。

场景 建议格式 原因
浏览器和后端的公开接口 JSON 调试方便,生态成熟,前端支持好
服务配置、CI 配置 YAML 人类可读,修改方便
对接传统系统或已有 XML 协议 XML 兼容既有接口和工具链
内部 RPC、服务间高频通信 Protobuf 协议明确,体积小,解析快
需要人工直接编辑的数据 YAML 或 JSON 文本格式,排查问题方便
需要跨语言且长期演进的数据 Protobuf 或 JSON 有成熟的兼容和生态方案

这里还有一个经常被忽略的判断:数据格式和传输协议不是一回事。 HTTP、TCP、消息队列解决"怎么传",JSON、XML、YAML、Protobuf 解决"传什么"。HTTP 请求体可以是 JSON,也可以是 XML;gRPC 通常使用 Protobuf,但 Protobuf 本身并不等于 gRPC。

统一处理错误

无论使用哪一种格式,序列化和反序列化都可能失败。实际项目中不要忽略错误:

复制代码
func encodePerson(person Person) ([]byte, error) {
	data, err := json.Marshal(person)
	if err != nil {
		return nil, fmt.Errorf("encode person: %w", err)
	}
	return data, nil
}

调用方可以根据错误内容记录日志、返回 HTTP 错误码或终止启动流程。配置文件解析失败时,通常应该让程序启动失败,因为带着错误配置继续运行,往往比直接报错更危险。

反序列化时还要注意三件事:

  • 输入数据可能为空、截断或格式错误;
  • 字段可能缺失,缺失字段通常会得到 Go 类型的零值;
  • 类型可能不匹配,例如把字符串传给 int 字段。

"能成功解析"不代表"数据符合业务规则"。例如 JSON 中 age: -1 可能是合法 JSON,但未必是合法业务数据。格式校验之后,仍然需要进行业务校验。

常见问题

为什么结构体字段必须大写

因为 encoding/xml、encoding/json 以及大多数第三方库都通过反射读取字段。小写字段对当前包外部不可见,编码器无法直接访问它们。

Marshal 和 MarshalIndent 有什么区别

两者都会把 Go 值序列化成 JSON 或 XML。Marshal 输出更紧凑,适合网络传输;MarshalIndent 会插入换行和缩进,适合日志、调试和人工阅读。

YAML 是 Go 官方标准库吗

不是。Go 标准库目前提供 encoding/json 和 encoding/xml,YAML 通常使用 gopkg.in/yaml.v3 等第三方包。

Protocol Buffers 能不能代替 JSON

不能简单地说"完全代替"。对外 API 通常更看重可读性、调试体验和浏览器生态,JSON 更合适;内部 RPC 更看重协议约束、体积和性能,Protobuf 更合适。两者解决的是相似问题,但工程取舍不同。

为什么 Protobuf 代码需要生成

.proto 文件是跨语言协议描述,protoc 会根据它生成 Go、Java、C++ 等语言的类型和编解码代码。生成代码把协议规则固定下来,减少手写解析代码和字段编号错误。

总结

XML、YAML、JSON 和 Protocol Buffers 都可以完成结构化数据的序列化和反序列化,但它们服务的场景不同:XML 重兼容,YAML 重可读,JSON 重通用,Protobuf 重效率和协议演进。

学习时可以先掌握标准库里的 encoding/json 和 encoding/xml,再使用 gopkg.in/yaml.v3 处理配置文件,最后通过 .proto、protoc 和 google.golang.org/protobuf 理解 Protobuf 的完整流程。

真正进行项目选型时,不要只看"哪个格式更快"。先问三个问题:数据是给人看的,还是给机器看的?协议是否需要跨语言长期演进?数据量和传输频率是否已经让文本格式成为瓶颈?答案通常比一张简单的性能排行表更可靠。

参考资料

相关推荐
我爱工作&工作love我1 小时前
P14359 [CSP-J 2025] 异或和
算法
小poop1 小时前
高级排序算法与性能对比——归并、非比较排序及总结
数据结构·算法·排序算法
aqiu1111111 小时前
CodeTop按顺序刷题0003:LeetCode 206. 反转链表(迭代法图文详解与动态演示)
算法·leetcode·链表
欢乐的小猪1 小时前
Linux命令 chmod
linux·运维·服务器
明航咨询—张老师1 小时前
工业控制系统安全工程师(CISP-ICSSE)全解析:报考条件、知识体系与职业价值
网络
silvamux_1 小时前
MCP 协议如何让大模型完成工具调用
大数据·服务器·elasticsearch·fastapi·vllm
guo_wen_qiang1 小时前
云服务器安装MongoDB-资源包安装
运维·服务器·mongodb
果霸大叔1 小时前
AI网关第8篇:AI 代理与负载均衡--多模型实例如何调度?
人工智能·算法·自然语言处理·负载均衡·ai-native
不会就选b1 小时前
算法日常・每日刷题--<动态规划>29
数据结构·算法·leetcode