半年多以前写过一篇《300 行代码实现一个消息队列》,实现了一个最简单的消息队列,能够把消息事件放到异步去处理。但随着业务的发展,简单的异步处理已经不能满足需求了:一条消息需要被多个不同的消费者消费。

按照原先的消息队列设计,要实现这个需求,要么是多注册几个队列,生产者往多个队列发消息;要么是在消费者那多开几个协程去分别处理。但这两种方案都高耦合,很不优雅。

优雅的方案应该是引入一个负责转发消息的中间层,生产者只需要将消息发给这个中间层,再由中间层转发给不同的队列(消费者),这个中间层我们称为 Exchange

阅读剩余部分

自从六年前换上 MikroTik RB750Gr3 之后就几乎没再折腾过给路由器(硬件)刷 OpenWrt,对于路由器刷机的技艺已经逐渐生疏。最近要帮同学的办公室换个路由器,选中了磊科 N60 Pro 这款容易刷的路由。

因为我有强迫症,所以没有选择网上别人提供在网盘上的固件,而是选择了 OpenWrt 官方的固件,正是因为网上关于刷 OP 官方固件的资料比较少,加上我很久没有碰 OpenWrt 了,差点刷成砖了(其实没有,我误以为是砖了),因此写下这篇记录,给同样有强迫症的朋友一些刷机参考。

阅读剩余部分

前阵子想做一个网站来放一些 referral 链接,研究了一下各种框架/现成的类 CMS 开源程序,Astro 又一次出现在视野中,于是决定试试看 Astro 到底是个什么东西。

最开始是找各种模板,先是用别人的模板搭了一个摄影集:https://splash.yian.me,当时觉得风格还不错,但是构建后的静态资源路径(Image 组件会对图片做处理,然后放在 /_astro/ 下)有点让强迫症不爽,然后因为各种原因暂时搁置了建站计划。

后来有点想找个静态博客替换掉 Typecho(单纯是不想再维护 PHP 环境了),恰好有人找我做网站,就决定要不自己亲自写一套 Astro 模板,也当是顺便熟悉一下 Astro,于是就有了一个新的技术博客:https://notes.yian.me。后面技术类的文章应该都会发在那边了,现在这个 Typecho 短期内还会继续维护。

然后过年期间又 vibe coding 了一个页面,至此最开始想做的 referral 的网站总算上线了:https://uses.yian.me,欢迎各位看官看看有没有被种草的东西,然后用我的链接注册一个😄

体验下来 Astro 是真棒,然后再托管在 Cloudflare Pages 简直完美。谢谢 Astro!

在做词焙查词接口时,如果词库不存在这个单词,则需要调用外部 API 更新词库,而目前用的大模型 API 调用时长较长(超过 5 秒)且不可控,故需要异步去做这件事。

这种场景很适合使用消息队列(RabbitMQ、Kafka、Redis PUB/SUB 等等),但为了使架构尽可能简单,不想轻易引入中间件,加上考虑到 Go channel 很适合在不同协程之间传递消息,于是使用 channel 封装了一个简易的消息队列:支持多队列、支持多消费者,而且整个封装才不到 300 行代码。

消息定义

消息队列,首先要有消息。最简单的一个消息就是 ID + 数据:

type Msg struct {
    ID   uint64
    Data any
}

消息 ID 用一个 uint64 类型的自增序列足矣。

队列定义

有了消息,接下来就可以定义队列了。

最简单的队列用一个 slice 用于保存消息,然再加上 EnqueueDequeue 即可:

type Queue struct {
    msgs []*Msg
}

func (q *Queue) Enqueue(msg *Msg) error
func (q *Queue) Dequeue() *Msg

但这样在并发的时候会出现问题,你可能会想到用一个 sync.Lock 来加锁,可行,但可以利用 chan 来实现无锁队列,具体原理是:新增 inout 两个 chan,然后在同一个协程里将 in 接收到的消息放入 msgs,用 out 来发送出队消息。

Queue 的定义如下:

阅读剩余部分

今天这篇与其说是月报,更像是年报 —— 不知不觉这是第 12 篇月报啦。

先是月报(2025.09.19~10.18),过去一个月做的东西并不多,大部分时间都花在了陪家人和思考,然后在零碎的时间主要是折腾 Homelab 服务器和路由器(又是跟 IPv6 折腾的时光),以及顺手迭代一下了 tipsy 的东西,抽空把 3 月份就说要写的文章给补上了:

再有就是更新了下简历,翻译了一份英文版出来。


阅读剩余部分

Go 在使用 os/exec 执行外部命令的时候,假如外部命令是持续输出的,该如何实时、持续获取外部命令的输出呢?

其实很简单,只需要通过 StdoutPipe() 创建一个管道接收外部命令的标准输出即可。

先来准备一个外部命令程序 ./hello/main.cc

#include "iostream"
#include <thread>

int main() {
    while (true) {
        auto now = std::chrono::system_clock::now();
        std::time_t now_c = std::chrono::system_clock::to_time_t(now);
        std::cout << "Current time is: " << std::ctime(&now_c) << std::flush;
        // sleep for a second
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

然后用 g++ 编译:g++ -o hello main.cc 得到 ./hello/hello,这个程序会每隔一秒输出当前时间。

接下来开始写我们的 Go 程序:

package main

import (
    "fmt"
    "os/exec"
    "strings"
)

func main() {
    cmd := exec.Command("./hello/hello")

    cmdStdout, err := cmd.StdoutPipe() // 创建一个管道接收外部命令的标准输出
    if err != nil {
        fmt.Println("Error creating StdoutPipe:", err)
        return
    }

    if err := cmd.Start(); err != nil {
        fmt.Println("Error starting command:", err)
        return
    }

    // 用协程非阻塞接收外部命令的流式输出
    go func() {
        for {
            buf := make([]byte, 1024)
            n, err := cmdStdout.Read(buf)
            if err != nil {
                fmt.Println("Error reading from stdout:", err)
                break
            }

            if n == 0 {
                continue
            }

            // 按需把尾部的换行符、或者是 '\0' (在 Go 里是 "\x00")去除掉
            output := strings.TrimSuffix(string(buf[:n]), "\n")
            fmt.Println("Output:", output)
        }
    }()

    if err := cmd.Wait(); err != nil {
        fmt.Println("Error waiting for command:", err)
        return
    }
}