Memoh 源码解读:一个多 Agent 平台的架构设计参考

本文基于 felinics/Memoh (commit 1aaef83)源码写作。所有代码引用均为仓库内相对路径与行号,可直接到 GitHub 对应行查看。 写作目标:一是拆解这个生产级多 Agent 平台的模块划分、权限设计、AI 调度与流转;二是提炼出你可以直接借鉴的设计模式。如果你没开发过 agent,先读第 1 章的入门部分。 0. 项目速览:Memoh 是什么 Memoh 是一个开源多 Agent 平台,核心卖点是给每个 AI agent 一台自己的"云电脑":独立的容器 workspace(文件系统、桌面、浏览器、网络),24/7 在线,通过 Telegram / Discord / 飞书 / 微信 / Matrix / 邮件等渠道对话,带长期记忆,支持定时任务,还能托管 Claude Code、Codex 等外部 CLI agent。 技术栈:Go + Echo + Uber FX(后端)、Vue 3(Web)、PostgreSQL(主存储)、Qdrant(向量记忆)、Docker/containerd/Apple Virtualization(workspace 容器)、可选 Redis(集群模式队列)。 代码规模:internal/ 下 76 个领域包,仅 internal/agent 就约 9MB 源码,是一个认真做过生产级调度设计的项目。这正是值得读它的原因。大多数开源 agent 项目把"调度"做成内存里的一个循环,而 Memoh 把 turn 的准入、所有权、幂等、恢复全部持久化到了 PostgreSQL,用数据库约束解决并发问题。这是本文最想讲清楚的部分。 1. Agent 运行原理入门(写给没写过 agent 的人) 在深入 Memoh 之前,先用它的真实代码把四个基础概念讲明白:编排(orchestration)、调度(scheduling)、上下文注入(context injection)、输出获取(output consumption)。 ...

2026-09-17 · 22 分钟 · DimAgent

用 Zvec 构建自己的 RAG 系统:Go 与 Rust 实战指南

Zvec 是阿里巴巴开源的嵌入式(进程内)向量数据库 —— 轻量、极速,可直接嵌入应用程序。本文将介绍如何使用 Zvec 的 Go 和 Rust SDK,从零搭建一套完整的 RAG(Retrieval-Augmented Generation)系统。 署名:本文由小米 MiMo-2.5-Pro 编写,Codex-5.5 审校。 什么是 RAG? RAG(Retrieval-Augmented Generation,检索增强生成)是一种将外部知识检索与大语言模型生成相结合的技术范式。其核心流程为: 索引(Indexing):将文档切分、向量化,存入向量数据库 检索(Retrieval):用户提问时,将问题向量化,在数据库中搜索最相关的文档片段 生成(Generation):将检索到的相关文档作为上下文,连同用户问题一起送入 LLM 生成回答 RAG 解决了 LLM 的几个核心痛点: 知识时效性:无需重新训练模型,通过更新知识库即可获取最新信息 幻觉问题:基于真实文档生成回答,大幅降低虚构内容的概率 可溯源性:回答可追溯到具体来源文档,增强可信度 为什么选择 Zvec? 在 RAG 系统中,向量数据库是核心组件。Zvec 相比其他方案有以下优势: 特性 Zvec 传统方案(Milvus/Qdrant/Weaviate) 部署模式 进程内嵌入,无需独立服务 需要单独部署服务 延迟 毫秒级,零网络开销 需要网络往返 依赖 零外部服务依赖(仍需链接 Zvec C 库) 需要 Docker/K8s 等 运维 零运维 需要运维团队 运行形态 随应用进程部署,数据本地持久化 依赖服务端实现 检索能力 向量 + 全文 + 混合检索 各有差异 对于中小型或单机优先的 RAG 系统,Zvec 的嵌入式架构意味着: ...

2026-06-16 · 10 分钟 · mimo-2.5-Pro, Codex-5.5

vanguard-go简明教程 by Claude

用一个 Go 服务器同时支持 REST、gRPC、gRPC-Web 和 Connect 客户端。本文使用claude code编写 什么是 vanguard-go? vanguard-go 是 ConnectRPC 团队开发的一个 Go 中间件库,核心功能是协议转码(Transcoding):它能让你只写一套 RPC 服务实现,就同时支持多种客户端协议: 客户端协议 是否支持 Connect Protocol ✅ gRPC ✅ gRPC-Web ✅ REST + JSON(HTTP 转码) ✅ 与 gRPC-Gateway 不同,vanguard 直接作为 net/http 中间件运行,无需额外的代理进程,性能更高,集成更简单。 核心概念 理解这三个概念就掌握了 vanguard 的精髓: Service(服务):对一个 Protobuf RPC 服务的配置包装,包含服务的 schema(用于协议转换)和实际的 HTTP 处理器。 Transcoder(转码器):将一组 Service 包装成 http.Handler,自动处理所有协议转换逻辑,也充当路由器。 HTTP Transcoding Annotations(HTTP 转码注解):在 .proto 文件里用 google.api.http 注解将 RPC 方法映射到 RESTful 路径,这是支持 REST 客户端的关键。 快速开始 1. 安装依赖 go get connectrpc.com/vanguard go get connectrpc.com/connect 如果你使用 gRPC-Go 服务器,还需要: ...

2026-04-09 · 6 分钟 · czyt

掌控 LLM 输出:API 层的干预手段

你买不到模型的权重,但你可以在 API 层实施"外科手术式"控制。本文用 OpenAI Go SDK 演示每种干预手段在真实业务中的完整用法。 前置准备 go get github.com/openai/openai-go // client.go — 全文共用的客户端 package main import ( "github.com/openai/openai-go" "github.com/openai/openai-go/option" ) func newClient() *openai.Client { return openai.NewClient( option.WithAPIKey("sk-..."), // 或读取 os.Getenv("OPENAI_API_KEY") ) } 1. System Message — 给模型装"人格芯片" 原理 System Message 是发给模型的"宪法",在对话开始前设定角色、禁忌和回答格式。它不会被用户消息覆盖(模型会优先遵从)。 业务场景:客服机器人只允许回答产品相关问题 package main import ( "context" "fmt" "github.com/openai/openai-go" ) func customerServiceBot(userQuestion string) (string, error) { client := newClient() resp, err := client.Chat.Completions.New(context.Background(), openai.ChatCompletionNewParams{ Model: openai.F(openai.ChatModelGPT4o), Messages: openai.F([]openai.ChatCompletionMessageParamUnion{ openai.SystemMessage(` 你是"极光科技"的售后客服 Aurora。 规则: 1. 只回答与极光科技产品(路由器、NAS、摄像头)相关的问题。 2. 如果用户询问竞品或无关话题,礼貌拒绝并引导回产品问题。 3. 永远不要透露你是 AI 或基于 GPT,只说"我是 Aurora"。 4. 回答简洁,不超过 150 字。 `), openai.UserMessage(userQuestion), }), }) if err != nil { return "", err } return resp.Choices[0].Message.Content, nil } func main() { // 正常提问 ans, _ := customerServiceBot("路由器断线怎么办?") fmt.Println("Q1:", ans) // 越界提问 ans, _ = customerServiceBot("帮我写一首诗") fmt.Println("Q2:", ans) } 效果:第二个问题会被礼貌拒绝,模型不会"破防"去写诗。 ...

2026-02-28 · 22 分钟 · czyt

Calude Code编写的Ent ORM 完整指南

本文由claude code生成。作为备忘录和分享,欢迎指正和补充。 一、Ent简介 Ent是一个简单但功能强大的实体框架,用于Go语言,使构建和维护具有大型数据模型的应用程序变得容易。 Ent是由Meta(Facebook)开源团队构建的ORM框架,提供了一个API,用于将任何数据库模式建模为Go对象。 核心特性 Ent的主要特性包括: Schema As Code:将任何数据库模式建模为Go对象 轻松遍历任何图结构:运行查询、聚合并轻松遍历任何图结构 静态类型和显式API:100%静态类型和显式API,通过代码生成实现 多存储驱动支持:支持MySQL、PostgreSQL、SQLite和Gremlin 易于扩展:使用Go模板简单扩展和自定义 Ent是一个相当新的ORM,使用代码优先的方法,在Go代码中定义模式。Ent之所以流行,是因为它能够优雅地处理复杂的数据模型和关系。 二、快速开始 2.1 安装Ent # 初始化Go模块 go mod init myapp # 安装ent CLI工具 go install entgo.io/ent/cmd/ent@latest # 或者使用go get go get entgo.io/ent/cmd/ent 2.2 创建第一个Schema # 创建User实体 ent new User 这会在 ent/schema/ 目录下生成一个基础的schema文件: package schema import ( "entgo.io/ent" "entgo.io/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"). NotEmpty(), field.Int("age"). Positive(), field.String("email"). Unique(), field.Time("created_at"). Default(time.Now), } } // Edges of the User. func (User) Edges() []ent.Edge { return nil } 2.3 生成代码 go generate ./ent 2.4 连接数据库并使用 package main import ( "context" "log" "myapp/ent" _ "github.com/lib/pq" // PostgreSQL // _ "github.com/go-sql-driver/mysql" // MySQL // _ "github.com/mattn/go-sqlite3" // SQLite ) func main() { // 连接PostgreSQL client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=myapp password=password sslmode=disable") if err != nil { log.Fatalf("failed opening connection to postgres: %v", err) } defer client.Close() // 运行自动迁移 if err := client.Schema.Create(context.Background()); err != nil { log.Fatalf("failed creating schema resources: %v", err) } ctx := context.Background() // 创建用户 user, err := client.User. Create(). SetName("张三"). SetAge(30). SetEmail("zhangsan@example.com"). Save(ctx) if err != nil { log.Fatalf("failed creating user: %v", err) } log.Println("user was created: ", user) } 2.5 使用SQLite3(非CGO驱动) 📋 使用场景 为什么使用非CGO的SQLite驱动? ...

2025-12-12 · 51 分钟 · czyt

在Go里面实现Google和Apple OAuth登录

本文由claude code编写,仅供参考。 在现代应用开发中,提供第三方登录已经成为标配功能。本文将详细介绍如何在Go语言中实现Google和Apple的OAuth登录,以及如何处理iOS和Android的原生登录。 一、OAuth 2.0 基础概念 OAuth 2.0是一个授权框架,允许应用在用户授权的情况下访问用户在第三方服务上的资源,而无需获取用户的密码。主要流程包括: 授权请求:引导用户到OAuth提供商的授权页面 授权确认:用户同意授权 获取授权码:重定向回应用并携带授权码 交换令牌:使用授权码换取访问令牌 访问资源:使用访问令牌获取用户信息 二、Google OAuth登录实现 2.1 前期准备 首先需要在Google Cloud Console创建项目并配置OAuth 2.0凭据: 访问 Google Cloud Console 创建新项目或选择现有项目 启用Google+ API或Google Identity服务 创建OAuth 2.0客户端ID(Web应用、iOS、Android分别创建) 配置授权重定向URI 2.2 安装依赖 go get golang.org/x/oauth2 go get golang.org/x/oauth2/google go get google.golang.org/api/oauth2/v2 2.3 Web端Google登录实现 package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "golang.org/x/oauth2" "golang.org/x/oauth2/google" oauth2api "google.golang.org/api/oauth2/v2" "google.golang.org/api/option" ) var googleOauthConfig *oauth2.Config func init() { googleOauthConfig = &oauth2.Config{ ClientID: os.Getenv("GOOGLE_CLIENT_ID"), ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"), RedirectURL: "http://localhost:8080/auth/google/callback", Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", }, Endpoint: google.Endpoint, } } // 处理Google登录请求 func handleGoogleLogin(w http.ResponseWriter, r *http.Request) { // 生成随机state参数,防止CSRF攻击 state := generateRandomState() // 将state存储到session中 saveStateToSession(r, w, state) // 生成授权URL url := googleOauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline) http.Redirect(w, r, url, http.StatusTemporaryRedirect) } // 处理Google回调 func handleGoogleCallback(w http.ResponseWriter, r *http.Request) { // 验证state参数 state := r.FormValue("state") savedState := getStateFromSession(r) if state != savedState { http.Error(w, "Invalid state parameter", http.StatusBadRequest) return } // 获取授权码 code := r.FormValue("code") if code == "" { http.Error(w, "Code not found", http.StatusBadRequest) return } // 交换令牌 token, err := googleOauthConfig.Exchange(context.Background(), code) if err != nil { http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) return } // 获取用户信息 userInfo, err := getGoogleUserInfo(token) if err != nil { http.Error(w, "Failed to get user info: "+err.Error(), http.StatusInternalServerError) return } // 处理用户信息(创建或更新用户) handleUserLogin(w, r, userInfo) } // 获取Google用户信息 func getGoogleUserInfo(token *oauth2.Token) (*GoogleUserInfo, error) { ctx := context.Background() // 创建OAuth2服务 oauth2Service, err := oauth2api.NewService(ctx, option.WithTokenSource(googleOauthConfig.TokenSource(ctx, token))) if err != nil { return nil, err } // 获取用户信息 userInfo, err := oauth2Service.Userinfo.Get().Do() if err != nil { return nil, err } return &GoogleUserInfo{ ID: userInfo.Id, Email: userInfo.Email, VerifiedEmail: userInfo.VerifiedEmail, Name: userInfo.Name, GivenName: userInfo.GivenName, FamilyName: userInfo.FamilyName, Picture: userInfo.Picture, }, nil } type GoogleUserInfo struct { ID string `json:"id"` Email string `json:"email"` VerifiedEmail bool `json:"verified_email"` Name string `json:"name"` GivenName string `json:"given_name"` FamilyName string `json:"family_name"` Picture string `json:"picture"` } 2.4 Android/iOS原生Google登录处理 对于移动端原生登录,客户端会使用Google SDK获取ID Token,然后发送给后端验证: ...

2025-12-12 · 15 分钟 · czyt

Hertz使用笔记

Hertz的优缺点 优点 中文社区维护 稳定,字节产品,有相关的开源项目参考,比如coze和coze studio hz的模板支持,使得代码生成更加灵活 缺点 工程体验上,不如go-kratos等项目。比如不能很好得支持buf等第三方生态。代码生成的使用场景,如多service模式下体验不好。 Template 模板 之所以将模板放在第一,是因为Hertz里面模板是过不去的坎。😄官方的一个mvc的template 或者 cwgo内置的模板 自动生成路由注册 新建一个template/package.yaml layouts: # 覆盖默认 router.go 模板 # 关键:将 Register 函数名改为包含服务名的唯一名称 - path: router.go delims: ['{{', '}}'] body: |- // Code generated by hertz generator. DO NOT EDIT. package {{$.PackageName}} import ( "github.com/cloudwego/hertz/pkg/app/server" {{- range $k, $v := .HandlerPackages}} {{$k}} "{{$v}}" {{- end}} ) /* This file will register all the routes of the services in the master idl. And it will update automatically when you use the "update" command for the idl. So don't modify the contents of the file, or your code will be deleted when it is updated. */ {{define "g"}} {{- if eq .Path "/"}}r {{- else}}{{.GroupName}}{{end}} {{- end}} {{define "G"}} {{- if ne .Handler ""}} {{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.HandlerMiddleware}}Mw(), {{.Handler}})...) {{- end}} {{- if ne (len .Children) 0}} {{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.GroupMiddleware}}Mw()...) {{- end}} {{- range $_, $router := .Children}} {{- if ne .Handler ""}} {{template "G" $router}} {{- else}} { {{template "G" $router}} } {{- end}} {{- end}} {{- end}} // Register register routes based on the IDL 'api.${HTTP Method}' annotation. func Register(r *server.Hertz) { {{template "G" .Router}} } # 覆盖 register.go 模板 # 在这个文件中聚合所有服务的 Register 调用 - path: register.go delims: ['{{', '}}'] update_behavior: type: "append" append_key: "service" insert_key: "{{.ServiceName}}" append_tpl: | {{$.DepPkgAlias}}.Register(r) body: |- // Code generated by hertz generator. DO NOT EDIT. package {{.PackageName}} import ( "github.com/cloudwego/hertz/pkg/app/server" {{$.DepPkgAlias}} "{{$.DepPkg}}" ) // GeneratedRegister registers routers generated by IDL. func GeneratedRegister(r *server.Hertz){ //INSERT_POINT: DO NOT DELETE THIS LINE! {{$.DepPkgAlias}}.Register(r) } 然后service proto都定义为不同的go package包, admin.proto ...

2025-10-30 · 3 分钟 · czyt

在并发Go应用中保持顺序【译】

原文链接 https://destel.dev/blog/preserving-order-in-concurrent-go 并发是 Go 语言的一大优势,但它带来一个根本性的权衡:当多个 goroutine 同时处理数据时,自然顺序会被打乱。大多数情况下,这并无大碍——无序处理已足够,且更快速、更简单。 但有时,顺序至关重要。 当顺序至关重要 以下是三个需要保持顺序至关重要的实际场景: 实时日志增强 :您正在处理高流量的日志流,通过数据库或外部 API 为每个条目添加用户元数据。顺序处理无法跟上输入速率,但并发处理会打乱顺序,使得增强后的日志对依赖时间顺序的下游消费者变得不可用。 在文件列表中查找首个匹配项 :您需要从云存储下载文件列表,并找到包含特定字符串的第一个文件。并发下载速度更快,但完成顺序是乱序的——第 50 个文件可能比第 5 个文件先完成,因此您不能简单地返回找到的第一个匹配项,因为无法确定更早的文件是否也包含该字符串。 时间序列数据处理 :这个场景激发了我的原始实现。我需要下载 90 天的交易日志(每个约 600MB),提取部分数据,然后比较连续日期的数据以进行趋势分析。顺序下载需要数小时;并发下载可实现数量级的速度提升,但会破坏我进行比较所需的时间关联性。 挑战很明确:我们需要在不牺牲结果顺序可预测性的前提下获得并发处理的速度优势。这不仅是理论问题——更是影响实际大规模系统的现实约束。 本文将探讨我在生产级 Go 应用中开发并采用的三种方法。我们将构建一个并发的 OrderedMap 函数,它能在保持顺序的同时将输入通道转换为输出通道,并支持具有背压机制的无限流处理。通过对每种方法进行基准测试,我们将理解其权衡取舍,并在此过程中发现令人惊讶的性能洞见。 问题:为何并发会破坏顺序 让我们快速回顾一下为什么并发会打乱顺序。原因之一是各个 goroutine 处理任务的速度不同。另一个常见原因——我们无法预测 Go 运行时如何精确调度 goroutine。 例如,goroutine #2 可能在 goroutine #1 完成第 10 项之前就处理完了第 50 项,导致结果顺序错乱。这是并发处理的自然行为。 若想查看实际效果,这里有一个在 Go Playground 上的快速演示 。 设计理念:背压与缓冲的权衡 传统的顺序并发方法采用某种重排序缓冲区或队列。当工作线程计算出结果但尚不能写入输出时,该结果会被暂存于缓冲区中,直至能够按正确顺序写入。 在这种设计中,缓冲区通常可以无限制地增长。这种情况发生在: 输入存在倾斜 – 早期项目的处理时间比后续项目更长 下游消费者处理速度较慢 另一种常见方法是将所有结果暂存于内存中(切片/映射等)再进行排序。但我们今天的目标是构建一个流式解决方案,它能够: 最小化延迟 – 结果一旦准备就绪立即输出 处理无限输入流 – 支持任意大甚至无限的输入(例如从标准输入或网络流读取) 保持内存受限 – 避免不必要地在内存中累积结果 话虽如此,下面介绍的算法是背压优先的。如果工作协程还无法将结果写入输出通道,它就会阻塞。这种设计受内存限制,并保持了开发者对 Go 通道行为的预期。 ...

2025-09-02 · 9 分钟 · czyt

iter.Seq实践【译】

原文链接 https://blog.vertigrated.com/iterseq-in-practice FirstN 返回序列中的前 N 个项。 // FirstN takes an iter.Seq[int] and returns a new iter.Seq[int] that // yields only the first 'limit' items without creating intermediate slices. func FirstN[T any](original iter.Seq[T], limit int) iter.Seq[T] { return iter.Seq[T](func(yield func(T) bool) { count := 0 for item := range original { if count < limit { if !yield(item) { return } count++ } else { return } } }) } SkipFirstN 跳过前 N 个元素并返回剩余的序列。 ...

2025-02-19 · 4 分钟 · czyt

在Go语言中处理带BOM的json数据

缘起 今天开发的时候遇到一个奇怪的问题,一个JSON文件,使用文本编辑器打开复制,并使用strings.NewReader来decode,是正常的,但是通过文件打开同样调用的方法来decode,却是失败的。后面通过打开IDE,发现文件前面有一些空白的内容。是一些bom信息。 关于BOM BOM (Byte Order Mark) 的历史原因和用途主要与字符编码和跨平台兼容性有关: 历史原因 Unicode 出现前: ASCII 只用 1 字节,没有字节序问题 各国有自己的编码标准(GB2312、Shift-JIS等) Unicode 引入后: UTF-16 使用 2 字节表示字符 不同CPU架构的字节序不同: Big Endian (大端序): 高位字节在前 Little Endian (小端序): 低位字节在前 跨平台问题: Intel x86 使用小端序 Motorola 68k 使用大端序 同一文件在不同平台解析可能出错 BOM 的作用 UTF-16 的字节序标记: FE FF: Big Endian FF FE: Little Endian UTF-8 的编码标识: EF BB BF: 表明这是 UTF-8 编码 UTF-8 实际不需要 BOM(字节序无关) Windows 添加 BOM 主要为了兼容性 实际例子 // 字符 "中" 在不同编码下的表示 text := "中" // UTF-8: E4 B8 AD // UTF-16BE: 4E 2D // UTF-16LE: 2D 4E // 示例代码 func showEncoding() { text := "中" utf8Bytes := []byte(text) // UTF-8 utf16beBytes := utf16.Encode([]rune(text)) // UTF-16BE fmt.Printf("UTF-8: % X\n", utf8Bytes) fmt.Printf("UTF-16BE: % X\n", utf16beBytes) } 常见问题场景 文本编辑器: Notepad++ 可选是否添加 BOM VS Code 可以识别和显示 BOM 某些编辑器可能显示 BOM 为特殊字符 Web 开发: PHP 文件开头的 BOM 可能导致 header 错误 HTML 文件的 BOM 可能影响布局 JavaScript 文件的 BOM 可能导致语法错误 配置文件: XML 文件的 BOM 可能影响解析 INI 文件的 BOM 可能导致配置读取错误 JSON 文件的 BOM 可能导致解析失败 建议 UTF-8 编码: 不建议使用 BOM UTF-8 是字节序无关的 已成为事实标准 新项目: 统一使用 UTF-8 无 BOM 明确规定编码要求 添加编码检测机制 老项目维护: 保持现有编码方式 添加编码处理代码 记录文件编码信息 解决方案 帮助函数 // 检测文件是否包含 BOM func HasBOM(filename string) (bool, string, error) { file, err := os.Open(filename) if err != nil { return false, "", err } defer file.Close() reader := bufio.NewReader(file) peek, err := reader.Peek(4) if err != nil && err != io.EOF { return false, "", err } // 检查不同的 BOM boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, "UTF-32BE": {0x00, 0x00, 0xFE, 0xFF}, "UTF-32LE": {0xFF, 0xFE, 0x00, 0x00}, } for encoding, bom := range boms { if len(peek) >= len(bom) && bytes.Equal(peek[:len(bom)], bom) { return true, encoding, nil } } return false, "", nil } // 添加 UTF-8 BOM func AddUTF8BOM(filename string) error { // 读取原文件内容 content, err := os.ReadFile(filename) if err != nil { return err } // 检查是否已有 BOM if len(content) >= 3 && bytes.Equal(content[:3], []byte{0xEF, 0xBB, 0xBF}) { return nil // 已有 BOM,不需要添加 } // 添加 BOM newContent := append([]byte{0xEF, 0xBB, 0xBF}, content...) return os.WriteFile(filename, newContent, 0644) } // 移除 BOM func RemoveBOM(filename string) error { content, err := os.ReadFile(filename) if err != nil { return err } // 检查并移除不同类型的 BOM boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, "UTF-32BE": {0x00, 0x00, 0xFE, 0xFF}, "UTF-32LE": {0xFF, 0xFE, 0x00, 0x00}, } for _, bom := range boms { if len(content) >= len(bom) && bytes.Equal(content[:len(bom)], bom) { // 移除 BOM return os.WriteFile(filename, content[len(bom):], 0644) } } return nil // 没有 BOM,不需要处理 } 纯文本文件调用 func main() { // 创建示例文件 content := []byte("Hello, 世界!") tempDir := os.TempDir() filename := filepath.Join(tempDir, "test.txt") // 写入原始内容 if err := os.WriteFile(filename, content, 0644); err != nil { log.Fatalf("Error writing file: %v", err) } defer os.Remove(filename) // 1. 检测原始文件 hasBOM, encoding, err := DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("Original file has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 2. 添加 BOM if err := AddBOM(filename); err != nil { log.Fatalf("Error adding BOM: %v", err) } fmt.Println("Added UTF-8 BOM to file") // 3. 再次检测 hasBOM, encoding, err = DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("After adding BOM: has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 4. 移除 BOM if err :=RemoveBOM(filename); err != nil { log.Fatalf("Error removing BOM: %v", err) } fmt.Println("Removed BOM from file") // 5. 最后检测 hasBOM, encoding, err = DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("After removing BOM: has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 读取并显示最终内容 finalContent, err := os.ReadFile(filename) if err != nil { log.Fatalf("Error reading final content: %v", err) } fmt.Printf("Final content: %s\n", string(finalContent)) } JSON解析 package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "log" "os" ) // 定义 JSON 结构 type Person struct { Name string `json:"name"` Age int `json:"age"` Hobbies []string `json:"hobbies"` } // BOM 处理函数 func skipBOM(reader *bufio.Reader) error { boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, } // 预读4个字节 peek, err := reader.Peek(4) if err != nil && err != io.EOF { return fmt.Errorf("reading BOM: %w", err) } // 检查并跳过 BOM for encoding, bom := range boms { if len(peek) >= len(bom) && bytes.Equal(peek[:len(bom)], bom) { _, err = reader.Discard(len(bom)) if err != nil { return fmt.Errorf("skipping BOM: %w", err) } log.Printf("Skipped %s BOM", encoding) return nil } } return nil // 没有 BOM } // 读取并解析 JSON 文件 func ReadJSONFile(filename string) ([]Person, error) { // 打开文件 file, err := os.Open(filename) if err != nil { return nil, fmt.Errorf("opening file: %w", err) } defer file.Close() // 创建带缓冲的读取器 reader := bufio.NewReader(file) // 处理 BOM if err := skipBOM(reader); err != nil { return nil, fmt.Errorf("handling BOM: %w", err) } // 解析 JSON var people []Person decoder := json.NewDecoder(reader) if err := decoder.Decode(&people); err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) } return people, nil } // 创建示例 JSON 文件 func createSampleJSONFile(filename string, withBOM bool) error { // 示例数据 people := []Person{ { Name: "张三", Age: 25, Hobbies: []string{"读书", "游泳"}, }, { Name: "李四", Age: 30, Hobbies: []string{"篮球", "音乐"}, }, } // 转换为 JSON jsonData, err := json.MarshalIndent(people, "", " ") if err != nil { return err } // 如果需要 BOM,添加 UTF-8 BOM if withBOM { jsonData = append([]byte{0xEF, 0xBB, 0xBF}, jsonData...) } // 写入文件 return os.WriteFile(filename, jsonData, 0644) } func main() { // 测试文件名 filename := "test.json" // 1. 创建带 BOM 的 JSON 文件 log.Println("Creating JSON file with BOM...") err := createSampleJSONFile(filename, true) if err != nil { log.Fatalf("Error creating JSON file: %v", err) } defer os.Remove(filename) // 2. 读取并解析 JSON log.Println("Reading JSON file...") people, err := ReadJSONFile(filename) if err != nil { log.Fatalf("Error reading JSON file: %v", err) } // 3. 打印结果 log.Printf("Successfully parsed %d records:", len(people)) for i, person := range people { fmt.Printf("%d. %s (age: %d) likes %v\n", i+1, person.Name, person.Age, person.Hobbies) } // 4. 创建不带 BOM 的文件测试 log.Println("\nCreating JSON file without BOM...") err = createSampleJSONFile(filename, false) if err != nil { log.Fatalf("Error creating JSON file: %v", err) } // 5. 再次读取 log.Println("Reading JSON file without BOM...") people, err = ReadJSONFile(filename) if err != nil { log.Fatalf("Error reading JSON file: %v", err) } // 6. 打印结果 log.Printf("Successfully parsed %d records:", len(people)) for i, person := range people { fmt.Printf("%d. %s (age: %d) likes %v\n", i+1, person.Name, person.Age, person.Hobbies) } }

2024-12-03 · 5 分钟 · czyt