从嵌入式到Go:我的技术探索与求职实战复盘

~韩雨萱
2026-01-04 09:25
阅读 412

去年秋招投简历时,我还在STM32的寄存器手册里打转。谁能想到,两个月前我居然入职了一家做分布式消息中间件的创业公司,每天和Kafka、etcd、gRPC打交道?作为一个硬件出身、靠裸机驱动吃饭的嵌入式工程师,转型Go开发这条路,踩过的坑比写的代码行数还多。

今天这篇不是什么高大上的架构演进史,就是个普通程序员在技术岔路口摸爬滚打的真实记录。如果你也正处在转型期、准备跳槽、或者刚拿到offer还在适应新环境——别慌,你不是一个人。


转型的导火索:一份被拒了17次的简历

说实话,我不是那种“早就规划好职业路径”的人。之前五年一直在搞工业控制板卡,写过SPI、I2C、CAN总线,调过示波器到凌晨三点,甚至能凭耳朵听出晶振频率有没有跑偏(开玩笑的)。但随着年龄逼近30,看着身边同事要么转管理要么搞AI,我开始焦虑了。

最直接的打击来自简历石沉大海。那会儿我试着往“嵌入式+云边协同”方向包装,结果投出去17份,连个面试邀约都没有。HR的回复千篇一律:“经验不符”。有一次我鼓起勇气问内推的朋友:“是不是我简历太‘硬’了?”他苦笑:“兄弟,现在连IoT岗位都要求会Kubernetes了。”

那一刻我意识到:光会操作寄存器已经不够了。如果想继续做技术,必须向软件层迁移。

于是,我给自己定了个目标:三个月内,用Go写出一个能跑的分布式服务,并把它写进简历。


第一次写Go:指针让我怀疑人生

从C语言转Go,按理说应该很顺滑。毕竟Go的语法简洁,没有继承、没有泛型(那时候还没正式支持),看起来比C++友好太多。可现实狠狠打了我脸。

最开始写个简单的HTTP服务,我就被nil pointer dereference干懵了。在嵌入式里,空指针解引用直接死机重启,但在Go里,它优雅地 panic 了,还附带完整的 stack trace。当时我在终端看到:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10b4a5c]

熟悉的味道,陌生的报错格式。我差点以为自己回到了调试单片机HardFault的日子。

更尴尬的是,我习惯性地用结构体嵌套+指针传递数据,结果Go的垃圾回收机制让我一度以为内存泄漏了。后来才明白:Go里尽量用值语义,除非真需要共享状态。这个观念转变花了我整整一周。

但好处是,Go的工具链真的香。go mod 管理依赖比手动拷.a文件强一万倍;pprof 分析性能瓶颈比用逻辑分析仪抓总线轻松多了;delve 调试器虽然不如JTAG直观,但至少不用焊飞线。


实战项目:做个简易的分布式任务调度器

为了证明自己“能干活”,我决定复刻一个简化版的Celery(Python生态里的任务队列)。核心需求就三个:

  1. 支持任务提交(Producer)
  2. 多Worker并发消费
  3. 任务状态可查询

我选了Redis做中间存储,etcd做服务注册发现,gRPC做内部通信。为什么不用Kafka?因为本地起Kafka太重,而Redis我熟——毕竟以前在设备上用它做过缓存。

服务注册发现:etcd初体验

嵌入式里哪有什么服务发现?IP地址都是写死的。所以第一次用etcd的lease和watch机制,感觉像打开了新世界。

// 注册服务到etcd
func RegisterService(cli *clientv3.Client, service string, addr string) {
    lease, err := cli.Grant(context.TODO(), 10)
    if err != nil {
        log.Fatal(err)
    }
    _, err = cli.Put(context.TODO(), "/services/"+service+"/"+addr, "", clientv3.WithLease(lease.ID))
    if err != nil {
        log.Fatal(err)
    }

    // 心跳续约
    go func() {
        ticker := time.NewTicker(5 * time.Second)
        for range ticker.C {
            _, err := cli.KeepAliveOnce(context.TODO(), lease.ID)
            if err != nil {
                log.Printf("KeepAlive failed: %v", err)
            }
        }
    }()
}

这段代码现在看有点糙,但当时能跑通的时候,我激动得差点给etcd源码点了个star。最坑的是lease超时设置——一开始设成30秒,结果本地调试断点一停,服务就被踢了。后来改成10秒+5秒心跳,才稳定下来。

任务队列:用Redis Streams还是List?

纠结了很久。Streams是Redis 5.0的新特性,天然支持消费者组,听起来很分布式。但我查了下生产环境Redis版本——很多公司还在4.x。稳妥起见,我用了LPUSH + BRPOP

// Worker消费任务
func (w *Worker) Consume() {
    for {
        result, err := w.redis.BRPop(context.Background(), 0, "task_queue").Result()
        if err != nil {
            log.Printf("BRPop error: %v", err)
            time.Sleep(time.Second)
            continue
        }
        taskID := result[1] // [key, value]
        w.processTask(taskID)
    }
}

这里有个隐藏坑:BRPOP 在连接断开时不会自动重连。我加了重试逻辑,但更优雅的做法其实是用Redigo的Pool配合Do方法。不过对当时的我来说,能跑就行。


面试现场:被问“怎么保证Exactly-Once”?

靠着这个小项目,我终于拿到了几个面试机会。其中一家问了个灵魂问题:

“你的任务队列怎么保证Exactly-Once语义?”

我当场愣住。在嵌入式里,我们常说“尽力而为”,因为硬件中断可能丢,信号可能抖。但分布式系统里,这可是致命问题。

我支支吾吾说可以用Redis事务+Lua脚本原子操作。面试官点点头,又问:“那如果Worker处理完任务,还没来得及ACK就挂了呢?”

完蛋,没考虑到故障恢复场景。

回家后我补了幂等性设计:每个任务带唯一ID,Worker处理前先查状态,处理完更新状态为“完成”。状态存储用Redis Hash,key是task_id,field包括status、result、timestamp。

func (w *Worker) processTask(taskID string) {
    // 幂等检查
    status, _ := w.redis.HGet(context.Background(), "task:"+taskID, "status").Result()
    if status == "completed" {
        return // 已处理,跳过
    }

    // 标记为处理中
    w.redis.HSet(context.Background(), "task:"+taskID, "status", "processing")

    // 执行业务逻辑
    result := doRealWork(taskID)

    // 标记完成(原子操作)
    script := `
        if redis.call("HGET", KEYS[1], "status") == "processing" then
            redis.call("HMSET", KEYS[1], "status", "completed", "result", ARGV[1])
            return 1
        end
        return 0
    `
    w.redis.Eval(context.Background(), script, []string{"task:" + taskID}, result)
}

这段Lua脚本成了我后来面试的“救命稻草”。每次被问到一致性,我就掏出它,再补充一句:“当然,生产环境我们会用数据库事务或Saga模式兜底。”


入职两个月:从“硬件佬”到“Go仔”的文化冲击

现在回头看,转型最难的不是技术,而是思维模式。

在嵌入式团队,大家信奉“资源有限,能省则省”——RAM就64KB,你还敢用动态内存?但在Go团队,PM张口就是“先上线,性能后面优化”。上周五晚上,测试同学提了个P0级bug:某个API在高并发下响应变慢。我第一反应是“是不是GC停顿太久?”,结果查了半天,发现是数据库没加索引。

运维同事还调侃我:“你这嵌入式老哥,debug方式还是太硬核了,动不动就想看底层指标。”

不过也有优势。比如我对时序特别敏感——知道网络延迟不可能低于光速,所以设计gRPC超时时会留足buffer;我也习惯做边界测试,比如模拟etcd宕机、Redis主从切换,这些在嵌入式里叫“异常工况测试”。


求职建议:简历怎么写才能过筛?

结合我被拒17次+最终上岸的经验,给转型者几点建议:

误区 正确姿势
写“精通STM32、FreeRTOS” 写“基于C/Go实现跨平台任务调度系统,支持服务注册、状态追踪”
强调硬件调试能力 强调“端到端问题定位能力”,举例:从API延迟到DB慢查询再到网络RTT
简历堆砌技术名词 用STAR法则:Situation(背景)、Task(任务)、Action(行动)、Result(结果)

比如我现在的简历项目描述:

分布式任务调度器(Go)

  • 基于Redis+etcd构建高可用任务队列,支持动态扩缩容Worker节点
  • 实现幂等处理与状态持久化,保障At-Least-Once语义,业务重试率<0.1%
  • 通过pprof优化内存分配,QPS从800提升至2200(4核8G机器)

你看,没提一句“我会SPI”、“我能画PCB”,但展示了工程能力和结果导向。


最后一点真心话

转型不是背叛过去,而是把老经验变成新武器。我现在写Go,还是会下意识考虑资源占用;看分布式协议,会类比CAN总线的仲裁机制。这些“过时”的知识,反而成了我的差异化优势。

如果你也在犹豫要不要跳出舒适区——别等了。哪怕只是今晚fork一个开源项目,读十行源码,都是向前的一步。

毕竟,在程序员的世界里,能跑的代码永远比完美的计划更有说服力

(完)

P.S. 上周团建,CTO听说我以前搞硬件,半开玩笑说:“下次服务器宕机,你能不能拿示波器测测网卡?” 我回他:“可以,但得加钱。”

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝