如何优雅地重复读取HTTP响应体

背景介绍 在Go语言中处理HTTP请求时,我们经常需要多次读取响应体(Response Body)的内容。然而,默认情况下Response Body只能被读取一次,这是因为它实现了io.ReadCloser接口,读取完毕后数据就会被消费掉。本文将介绍一个优雅的解决方案。 问题描述 假设我们有以下场景: 需要在日志中记录API的响应内容 需要对响应内容进行多次处理 需要在中间件中查看响应内容,同时不影响后续处理 解决方案 我们可以使用io.TeeReader来实现响应体的复制,这样就能多次读取相同的内容。以下是具体实现: package main import ( "bytes" "fmt" "io" "net/http" ) func DupReadCloser(reader io.ReadCloser) (io.ReadCloser, io.ReadCloser) { var buf bytes.Buffer tee := io.TeeReader(reader, &buf) return io.NopCloser(tee), io.NopCloser(&buf) } func DupResponseBody(resp *http.Response) ([]byte, error) { var buf io.ReadCloser resp.Body, buf = DupReadCloser(resp.Body) data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } resp.Body = buf return data, nil } 代码解析 DupReadCloser函数:接收一个io.……

阅读全文

Jetbrains 非预期 format 代码过滤

在升级到 2025.1 系列版本后,我们项目中的 .proto 文件出现了一个令人头疼的问题:仅仅修改几行内容,却引发了整个文件的大面积 format 变化。 由于我们使用的是公共 repo,我们一直非常注重 diff 的最小化,避免格式化带来的冗余变更。但这次 .proto 文件却被“悄悄”改了样子。 ❓ 发生了什么? 我们原本以为是 protobuf 插件导致的自动格式化,于是尝试关闭 .proto 文件的相关插件支持,退回纯文本模式。然而,问题依旧存在。 在多次对比 format 前后的差异后,我们终于发现—— 原来是 所有注释行最后的空格被自动移除了。 也就是说,哪怕我们只改了一行逻辑代码,整个文件中尾部有空格的注释行也被一并“清理”,导致 Git diff 看起来像是大动干戈,实际只是视觉污染。 ✅ 解决方案:添加 .editorconfig 为了彻底杜绝这类问题,我们最终采用了 .editorconfig 文件配置来控制编辑器行为。 在项目根目录下创建 .editorconfig 文件,内容如下: [*] trim_trailing_whitespace = false 这条配置的意思是:对所有文件类型,不自动清除行尾空格。 配置完成后,记得重启编辑器,确保设置生效。……

阅读全文

解决 SELinux 拦截导致 systemctl 启动服务失败的问题

SELinux 与 systemctl 执行问题的解决方案 在系统管理中,SELinux(Security-Enhanced Linux)是一种强制访问控制(MAC)安全机制。尽管它提供了更高的安全性,但有时也会引发一些问题。本文将介绍一个常见的问题,并详细说明如何通过日志确认问题并修正它。 问题描述 我们遇到了一个问题,即可以直接使用命令执行某个服务,但当尝试使用 systemctl start caddy 命令启动时,却会出现以下错误: (code=exited, status=203/EXEC) 日志确认问题 要确认是否是SELinux导致的问题,可以通过查看系统的审计日志。在终端中使用以下命令: grep 'avc: ' /var/log/audit/audit.log 你会看到类似下面的日志条目: type=AVC msg=******: avc: denied { execute } for pid=**** comm="(caddy)" name="caddy" dev="dm-0" ino=**** scontext=********* tclass=file permissive=0 这条日志中的信息显示,SELinux 拒绝了 caddy 服务执行的请求。 解决方案 为了解决这个问题,我们可以采取临时方案,即将需要执行的文件放置在 /usr/bin 目录下。以下是具体步骤: 将 caddy 文件移动到 /usr/bin 目录下: sudo mv /path/to/caddy /usr/bin/ 执行 restorecon 命令,恢复文件的SELinux安全上下文: sudo restorecon -Rv /usr/bin restorecon 命令会根据SELinux策略恢复指定目录下文件的默认安全上下文。在执行完这个命令之后,你放在 /usr/bin 下的 caddy 文件就可以正常使用了。 为什么放在 /usr/bin 下才能用? SELinux 对系统中的每个文件和进程都施加了安全上下文。当你将文件放置在 /usr/bin 目录下时,系统会自动为这些文件分配合适的安全上下文,使得它们可以被系统服务执行。而自定义目录中的文件可能没有正确的安全上下文,因此被SELinux拒绝执行。……

阅读全文

adguard home 安装与配置

引言 AdGuard Home 是一款网络级广告和跟踪器拦截器。它可以帮助你在家中或办公室的网络上拦截广告,保护隐私,并且提升上网速度。选择 AdGuard Home 的原因包括它的开源性质、易于配置和强大的功能。 安装 部署我们选择 docker + caddy 的方式 使用 docker 部署 adguard adguard 提供了 docker 部署的方式: github地址 建议使用 docker run --name adguardhome --restart unless-stopped -v /root/adguardhome/work:/opt/adguardhome/work -v /root/adguardhome/confdir:/opt/adguardhome/conf -p 53:53/tcp -p 53:53/udp -p 8080:80/tcp -p 4443:443/tcp -p 4434:443/udp -p 3000:3000/tcp -d adguard/adguardhome 配置 caddy yourdomain.com { reverse_proxy 127.0.0.1:3000 配置 adguard 访问 http://yourdomain.com:3000 进行配置 访问 admin 配置页面 修改 CaddyFile 文件,修改为配置 yourdomain.com { reverse_proxy 127.0.0.1:8080 配置 adguard 配置 DOH 需要配置上, 证书和密钥, 证书可以使用 acme.……

阅读全文

golang json.Marshal(error) 返回 `{}`, 问题分析与解决方案

序列化 Go 语言中的 error 接口问题:问题发现、原因与解决方案 在开发 Go 语言项目时,我们常常需要将结构体序列化为 JSON 格式。然而,当结构体中包含 error 接口时,序列化结果往往会不如预期。在这篇博客中,我们将讨论这个问题的原因,并提供两种解决方案。 问题描述 我们有一个结构体 CustomError,其中包含一个 error 类型的字段。如下所示: package main import ( "encoding/json" "fmt" ) type CustomError struct { UserID int64 Err error } func (e *CustomError) Error() string { return fmt.Sprintf("user %d: %s", e.UserID, e.Err.Error()) } func main() { var errs []CustomError errs = append(errs, CustomError{UserID: 1, Err: fmt.Errorf("error 1")}) errs = append(errs, CustomError{UserID: 2, Err: fmt.Errorf("error 2")}) errs = append(errs, CustomError{UserID: 3, Err: fmt.……

阅读全文

使用 docker 搭建 filebrowser

filebrowser 是一个文件管理器,可以通过 web 界面管理文件,支持文件上传、下载、预览等功能。 详情可以参考github:github 使用 docker 搭建 filebrowser 先初始化配置文件和数据库文件 这个在 https://filebrowser.org/installation 上没有说明, 如果直接启动将直接失败 mkdir -p /root/filebrowser touch /root/filebrowser/filebrowser.db touch /root/filebrowser/config.json echo '{ "port": 80, "baseURL": "", "address": "", "log": "stdout", "database": "/database.db", "root": "/srv" }' > /root/filebrowser/config.json 使用 docker 启动 filebrowser docker run \ -d --name=filebrowser \ -v /root/filebrowser/file:/srv \ -v /root/filebrowser/filebrowser.db:/database.db \ -v /root/filebrowser/config.json:/.filebrowser.json \ -u $(id -u):$(id -g) \ -p 8081:80 \ --restart=always \ filebrowser/filebrowser 查看日志, 看到如下日志就证明已经启动成功了 2024/05/26 15:38:15 Warning: filebrowser.……

阅读全文

博客迁移到 cloudflare pages

最近把博客迁移到了cloudflare pages,感觉还不错,速度也挺快的。 顺便记录下这个过程。 需要先在 github 上创建好仓库, 可以参开这里。 在 cloudflare pages 上创建一个应用, 连接到 github 上的仓库。 选择使用 hugo 静态网站生成器。 ……

阅读全文

搭建了一个 chat-next-web 服务, 支持 GPT-4

最近, 我搭建了一个 chat-next-web 服务, 支持 GPT-4, 你可以在这里https://chat.twotigers.xyz体验一下 服务是基于 ChatGPT-Next-Web 当然你可以用你喜欢的客户端来进行链接 进入后进行一次设置 , Endpoint && Key 可以关注微信公众号 代码日记 发送 gpt 关键词获取 然后就可以开始聊天了……

阅读全文

使用 cf 来实现无限多个私人定制邮箱

拥有个私人定制的邮箱一直是一件很酷的事情, 除了能彰显个性, 还能让你的邮箱更加安全, 例如我们在注册一些网站的时候, 我们想用一些临时邮箱来代替 临时邮箱固然能带来一些便利, 但是也有一些缺点, 例如: 持久化和安全性, 临时邮箱的安全性是很低的, 你的邮件可能会被别人看到, 有些临时邮箱也不会持久化, 你的邮件可能会在几天后就消失 那么有没有一种方法能够让我们拥有无限多个私人定制的邮箱呢? 答案是肯定的, 本文将会介绍如何使用 cf 来实现无限多个私人定制邮箱 那么我们首先需要一个域名, 例如: example.com, 购买域名的方法很多, 这里就不再赘述了, 本文将会以 example.com 为例 第二步, 使用 cf 来托管你的域名 这里选择免费的账户就可以了 按照提示下一步就可以了, 需要在你注册域名的网站更换 DNS 服务器 注意: 这个过程时间肯能会比较久一些, 请耐心等待 第三步, 在 cf 开启邮箱服务, 需要注意首次使用时会提示你缺少相应的 DNS 记录, 直接点击添加就可以了, 会自动添加相关记录 进入 routing rules 点击 crate address 在上面的配置都完成之后, 会提示让你去校验邮箱地址, 至此, 你的邮箱就已经开通了, 可以使用你刚刚配置的邮箱地址来收取邮件了, 邮件会发送到你刚刚绑定的邮箱里面 等等, 我们标题上说的无限多个私人定制邮箱呢? 难道要手动添加吗? 当然不是 cf 提供了获取所有前缀的功能, 开启后 任意前缀@example.com 都可以收到邮件了, 现在我们来开启下 在这里配置好之后, 我们就可以使用任意前缀@example.……

阅读全文

使用 docker 部署 kafka

在很多时候我们需要使用到消息队列, 其中 kafka 是一个非常优秀的消息队列, 在我们平时开发中也经常会用到, 但是在开发环境中部署 kafka 是一个非常麻烦的事情 在 kafka 官网上, 有一个详细的部署文档, 需要的小伙伴可以参考这个文档 https://kafka.apache.org/quickstart, 但是随之带来的问题是, 我们需要 java 环境, 并且需要安装 zookerper, 那么如果我仅仅是想本地开发环境中使用 kafka, 有没有更简单的方法呢? 为了解决这个问题, 我们可以使用 docker 来部署 kafka 使用 docker 来部署 kafka 的测试节点, 我们需要先安装 docker 和 docker-compose, 这里就不再赘述了, 请自行安装 首先我们在已经安装好 docker 和 docker-compose 的机器上, 创建一个目录, 并且在这个目录下创建一个 docker-compose.yml 文件, 文件内容如下 version: "2" services: kafka: image: docker.io/bitnami/kafka:3.6 ports: - '9094:9094' volumes: - "kafka_data:/bitnami" environment: # KRaft settings - KAFKA_CFG_NODE_ID=0 - KAFKA_CFG_PROCESS_ROLES=controller,broker - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 # Listeners - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT volumes: kafka_data: driver: local 然后我们就可以在这个目录下执行 docker-compose up -d 来启动 kafka 了, 启动完成后, 我们可以使用 docker ps 命令来查看 kafka 是否启动成功……

阅读全文