[GoLang避坑实战-19] 搞定基础就想收工?别天真了!揭秘 Go 进阶路上的“九九八十一坑”

大家好,我是极客老墨! 恭喜你,跟着老墨走完了 Go 语言的基础教程。从变量声明到实战开发CLI程序,从简单的 if-else 到初探并发,你已经拿到了进入 Go 世界的入场券。 但前几天有个读者私信我,一句话把我问住了: “老墨,基础教程我看完了,感觉自己无敌了,接下来是不是可以直接去面大厂拿 50K 了?” 我当时正在喝咖啡,差点没一口喷出来。 兄弟,听老墨一句劝:搞定语法只是让你学会了“拿刀”,但要想在工程实战里“切口如丝”,你离真正的 Go 高手还差着好几个“坑”呢。 今天这篇,咱们不讲新语法,咱们聊聊实话:基础完了,然后呢? 避坑回马枪:那些“以为懂了”的基础坑 在聊进阶之前,老墨不放心,得带你杀个回马枪。下面这几个基础篇里的“躲猫猫”,你真的彻底搞定了吗? 影子变量 (Shadowing):你是不是还在 if err := ... 里不小心遮蔽了全局变量,导致逻辑死活跑不通? 切片共享底层数组:你是不是还在愉快地 s2 := s1[:2],结果改了 s2 发现 s1 也崩了? 循环变量地址问题:虽然 Go 1.22 帮你填了坑,但你如果还没养成“局部副本”的意识,在老版本项目里照样会翻车。 defer 的陷阱:你是不是还在循环里写 defer?或者在 defer 里漏写了 nil 检查? 老墨建议:如果上述几个场景你还得查笔记,那说明你的基础还没到“化境”。去 go-tutorial-code 把对应的演示代码再跑一遍,那是老墨用 Bug 堆出来的血泪史。 能写和能打,是两回事 很多同学觉得自己“会写”了,但一上手真实项目就抓瞎。 第一次写 Web:信心满满开了 10 万个 Goroutine,结果服务器内存瞬间爆炸。这时你才知道,原来 Golang 的并发不是让你无脑 go func()。 第一次面大厂:面试官问你:“GMP 调度器是怎么处理阻塞的?”你一脸懵逼,心想我只要会写 API 不就行了? 第一次做架构:代码一多就乱成一团,依赖注入 (DI) 的意义在哪?接口到底该怎么抽象? 这就是**“语法选手”和“工程选手”**的区别。基础教程教你“刀怎么拿”,而进阶之路教你“仗怎么打”。 老墨的进阶地图:内功与外家功夫 想成为真正的 Go 高手,老墨认为必须“双修”: ...

2026-03-15 · 1 min · 172 words · 老墨

Go模糊测试实战:让AI帮你找Bug

大家好,我是极客老墨。 之前写过一篇Go 测试:写得爽,跑得快,聊了单元测试、表格驱动测试和基准测试。很多朋友留言问:“老墨,单元测试我会写了,但总感觉测不全,怎么办?” 这就是今天要聊的:模糊测试(Fuzzing)。 写代码爽,写测试累。更累的是,你辛辛苦苦写了一堆单元测试,覆盖率看起来挺高,结果上线还是出Bug。为啥?因为你只测了你能想到的场景,没测你想不到的。 Go 1.18 开始,官方直接把模糊测试内置了。它会自动生成海量随机输入,帮你找到那些"想不到的 Bug"。不需要装第三方库,go test 就能跑。 什么是模糊测试?一句话说清楚 模糊测试就是让程序自己生成各种奇葩输入,疯狂测试你的代码,直到找出Bug为止。 传统单元测试:你说测什么就测什么。 模糊测试:程序自己想办法搞你,直到搞出问题。 从Go 1.18开始,Go官方在标准库里内置了Fuzzing支持。不需要装第三方工具,开箱即用。 为什么要用模糊测试? 老墨先给你看个真实案例。 假设你写了个URL解析函数,单元测试写了10个case,都通过了。结果上线后,用户输入了一个带emoji的URL,程序直接panic。 为什么?因为你的测试用例里没有emoji。 单元测试的局限性: 只能测你想到的场景 边缘情况容易遗漏 维护成本高(每个case都要手写) 模糊测试的优势: 自动生成海量测试数据 能发现你想不到的边缘情况 特别擅长找安全漏洞(SQL注入、缓冲区溢出等) 实战案例:一个看似简单的字符串反转函数 我们来写个最简单的函数:反转字符串。 1func Reverse(s string) string { 2 b := []byte(s) 3 for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { 4 b[i], b[j] = b[j], b[i] 5 } 6 return string(b) 7} 看起来没问题吧?我们写个单元测试: 1func TestReverse(t *testing.T) { 2 testcases := []struct { 3 in, want string 4 }{ 5 {"Hello, world", "dlrow ,olleH"}, 6 {" ", " "}, 7 {"!12345", "54321!"}, 8 } 9 for _, tc := range testcases { 10 rev := Reverse(tc.in) 11 if rev != tc.want { 12 t.Errorf("Reverse: %q, want %q", rev, tc.want) 13 } 14 } 15} 运行测试: ...

2026-02-26 · 5 min · 936 words · 老墨

Go模块管理踩坑实录:从入门到放弃再到真香

大家好,我是老墨。 很早之前,翻译了一篇 Golang 官方的模块管理文档中文,那篇有点“官方”了,今天老墨搞一篇接地气的模块实战教程。 说实话,我第一次接触 Go 模块的时候,内心是拒绝的。 GOPATH 那套虽然老,但好歹能用。突然冒出来个 go.mod、go.sum,还有什么 replace、indirect,看着就头大。更别提那些看起来像乱码的版本号:v1.2.3-0.20210101000000-abcdef123456,这是什么鬼? 但折腾了几个月后,我发现:真香。 今天就把我踩过的坑、学到的经验,全部掏出来。不讲理论,只讲实战。 为什么要用Go模块? 先说说为什么要用模块,而不是继续用GOPATH。 GOPATH 的三大痛点 版本管理是灾难 所有依赖都在 $GOPATH/src 下 同一个包只能有一个版本 项目 A 用 v1.0,项目 B 用 v2.0?抱歉,做不到 依赖地狱 依赖的依赖的依赖……谁也不知道用了什么版本 换台机器编译?祝你好运 团队协作?每个人的依赖都不一样 无法复现构建 今天能编译,明天就不行了 因为依赖包更新了 你根本不知道 Go 模块解决了什么 每个项目独立管理依赖 版本号明确,可复现构建 依赖关系清晰,记录在 go.mod 校验和保证安全性(go.sum) 支持私有仓库和代理 快速上手:5分钟创建第一个模块 1. 初始化模块 1# 创建项目目录 2mkdir myproject 3cd myproject 4 5# 初始化模块 6go mod init github.com/yourusername/myproject 这会生成一个go.mod文件: 1module github.com/yourusername/myproject 2 3go 1.21 就这么简单。 2. 添加依赖 写点代码,导入个包: 1package main 2 3import ( 4 "fmt" 5 "github.com/gin-gonic/gin" 6) 7 8func main() { 9 r := gin.Default() 10 r.GET("/", func(c *gin.Context) { 11 c.JSON(200, gin.H{"message": "Hello"}) 12 }) 13 r.Run() 14} 然后: ...

2026-02-25 · 4 min · 751 words · 老墨

[GoLang避坑实战-00] 别再犹豫了!一个老码农的真心话:为什么 2026 年必学 Go

大家好,我是极客老墨。 在互联网行业摸爬滚打十几年,从早期的LAMP时代,到Java的Spring全家桶,再到后来的云原生浪潮,老墨见证了技术的每一次变迁。 最近很多朋友问我:“老墨,现在的AI写代码这么厉害,大模型日新月异,智能IDE更是百花齐放,Claude Code、Cursor 简直是神,还需要专门去学一门后端语言吗?如果学,学什么比较好?Java还是Python?或者 Rust?” 这是一个非常好的问题。今天老墨就结合自己的经验,跟大家聊聊为什么在这个AI横行的时代,我依然建议大家掌握Golang,并且会手把手教你如何拿下它。 1. AI时代,为什么还要学后端语言? 很多同学觉得,现在AI不仅能生成CRUD代码,甚至能帮你完成全套编码、测试,这样的发展速度,我真的有必要学编程?我只需要会写Prompt不就行了吗? 大错特错。 AI确实能提高效率,但它目前还无法替代架构思维和底层认知。 知其然,更要知其所以然:AI生成的代码,如果出了Bug,或者性能不达标,你看不懂怎么调优? 不仅仅是Coder,更是Engineer:单纯的写代码(Coding)会被AI取代,但工程化能力(Engineering)——包括系统设计、并发处理、错误治理、服务部署,是AI很难完全掌控的。 掌握控制权:作为一名开发者,你不能做AI的傀儡。你需要有能力判断AI生成的代码是垃圾还是金子。 掌握一门强类型的、编译型的后端语言,能让你深入理解计算机的工作原理:内存管理、进程线程、网络协议。这些内功,是Prompt Engineering给不了你的。 2. 为什么是Golang? 在众多后端语言中,老墨首推 Go (Golang)。不是因为赶时髦,而是基于实用主义的考量。 Java:沉重的企业级战车 Java确实强大,生态无敌,将近30年了仍然是一门非常活跃的开发语言。但是: 太重了:Spring Boot启动一下,内存吃掉几百兆是常事。对于想要快速开发微服务或者云原生应用的极客来说,有点“大炮打蚊子”。 语法繁琐:虽然有了Lombok和新版本的语法糖,而且语法糖、新特性在一直增加,就是为了简化开发、提高效率,但比起Go的简洁,Java依然显得啰嗦。 JVM调优是玄学:GC调优、JVM参数配置,是一门高深的学问,对于初学者来说门槛较高。 Rust:陡峭的绝壁 Rust绝对是好语言,内存安全,性能极致。但是: 学习曲线太陡峭:所有权(Ownership)、借用(Borrowing)、生命周期(Lifetime),这些概念能劝退90%的初学者。老墨到现在仍然还在学习 Rust,深有体会! 开发效率:为了通过编译器的检查,你可能需要花费大量时间与编译器搏斗。对于大多数互联网业务应用来说,Rust的开发效率不如Go。 Golang:平衡的艺术 Go语言是Google出品,有着纯正的工程血统。 简单直接:只有25个关键字(早期),语法极其简洁,没有花哨的语法糖。任何Go程序员写的代码,风格都惊人的一致,这在团队协作中是巨大的优势。 天生并发:go func(),一个Goroutine开启并发,Channel进行通信。这是我见过的处理并发最优雅的方式,没有之一。 性能强悍:编译型语言,接近C/C++的性能,但开发效率接近Python。 云原生通用语:Docker、Kubernetes、Prometheus…这些云原生时代的基石,全是Go写的。学了Go,你就拿到了通往云原生世界的门票。 老墨总结: 如果你想快速构建高性能的后端服务,不想被复杂的语法和繁重的运行时拖累,Go是你的不二之选。 3. 极客老墨的Golang学习路径 很多同学这就去买书了,别急!听老墨一句劝:不要一开始就啃大骨头! 现代语言学习,讲究的是 “Learn by Doing”。 第一阶段:不仅是Syntax,更是思维转变(1-2周) 不要死记硬背语法。重点理解Go独特的概念: 接口(Interface):Duck Typing(鸭子模型),非侵入式接口,这和Java的implements完全不同。 Goroutine & Channel:不要用共享内存来通信,要用通信来共享内存。这是Go并发的核心哲学。 Defer & Panic:Go没有try-catch,适应它的错误处理机制。 第二阶段:标准库是最好的老师(2-3周) Go的标准库(Standard Library)写得非常漂亮。重点攻克: net/http:几行代码起一个Web Server。 fmt, io, bufio:理解IO操作。 encoding/json:JSON处理是后端日常。 context:重中之重! 并发控制、超时处理全靠它。 第三阶段:工程化实战(1个月) 光会写Hello World没用,你需要能干活的框架: ...

2026-02-10 · 1 min · 179 words · 老墨

Go高级教程:反射 (Reflection) 实战

大家好,我是极客老墨。 反射 (Reflection) 赋予了程序在运行时检查和修改自身状态的能力。从 JSON 解析到 ORM 框架(如 GORM),再到依赖注入,它们的底层都离不开反射。有人说"反射是魔鬼",也有人说"没有反射,就没有现代 Web 框架"。这篇就带你拆解 reflect 包,学会如何在运行时动态操作对象。 1. 核心概念:Type 和 Value 在 reflect 包中,有两位绝对主角: reflect.Type:这是啥?(类型信息,如 int, string, User) reflect.Value:这值多少?(具体的数据,如 42, “hello”, User{Name:“Hank”}) 一切反射操作的起点都是 interface{}。 1package main 2 3import ( 4 "fmt" 5 "reflect" 6) 7 8func main() { 9 x := 3.14 10 11 // 1. 获取类型 12 t := reflect.TypeOf(x) 13 fmt.Println("Type:", t) // float64 14 15 // 2. 获取值 16 v := reflect.ValueOf(x) 17 fmt.Println("Value:", v) // 3.14 18} graph LR subgraph iface ["interface{}"] direction TB TypePtr["_type pointer"] DataPtr["data pointer"] end TypePtr -->|"reflect.TypeOf"| RType["reflect.Type"] DataPtr -->|"reflect.ValueOf"| RValue["reflect.Value"] style iface fill:#f9f9f9,stroke:#333,stroke-width:2px,color:#333 style TypePtr fill:#e1f5fe,stroke:#01579b,color:#01579b style DataPtr fill:#e1f5fe,stroke:#01579b,color:#01579b style RType fill:#fff9c4,stroke:#fbc02d,color:#333 style RValue fill:#fff9c4,stroke:#fbc02d,color:#333 2. 三大反射定律 Go 的反射有三条铁律(出自 Rob Pike): ...

2026-01-28 · 3 min · 485 words · 老墨

Go高级教程:其他并发工具

大家好,我是极客老墨。 Goroutine 和 Channel 是 Go 并发的基础,但有些场景它们不够用。频繁创建对象导致 GC 压力大?用 sync.Pool。并发读写 map 会 panic?用 sync.Map。简单的计数器用锁太重?用 atomic。 这篇就聊聊 Go 的高级并发工具,看看它们各自适合什么场景。 sync.Pool:对象复用 频繁创建和销毁大对象会给 GC 带来压力,sync.Pool 用于对象复用。 基本用法 1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8// 创建对象池 9var bufferPool = sync.Pool{ 10 // New 函数:池为空时创建新对象 11 New: func() interface{} { 12 fmt.Println("Creating new buffer") 13 return make([]byte, 1024) 14 }, 15} 16 17func main() { 18 // 从池中获取对象 19 buf := bufferPool.Get().([]byte) 20 21 // 使用对象 22 copy(buf, []byte("Hello, World!")) 23 fmt.Println(string(buf[:13])) 24 25 // 归还到池中 26 bufferPool.Put(buf) 27 28 // 再次获取(会复用刚才的对象) 29 buf2 := bufferPool.Get().([]byte) 30 fmt.Println(string(buf2[:13])) // 还是 "Hello, World!" 31} 要点: ...

2025-12-20 · 9 min · 1831 words · 老墨

Go高级教程:深入理解 GMP 调度器

大家好,我是极客老墨。 为什么 Go 语言能轻松支撑百万并发?为什么 Goroutine 切换成本这么低?这一切的背后,都站着一位神秘的大管家——GMP 调度器。这篇就用通俗易懂的语言,配合生动的比喻,带你深入理解 Go 高并发的核心秘密。 1. 为什么需要 GMP? 在很久很久以前(其实也就几十年前),我们写代码都是直接跟 线程 (Thread) 打交道。线程是操作系统(OS)调度的最小单位。 但是,线程这玩意儿太"贵"了: 内存占用高:一个线程栈大概要几 MB。 切换成本大:线程切换需要陷入内核态,保存寄存器、上下文,这简直就是"劳民伤财"。 这时候,Go 语言的设计师们拍案而起:“我们要造一种更轻量的线程!” 于是,Goroutine (协程) 诞生了。它初始只要几 KB,切换成本极低。 这就带来了一个问题:操作系统只认识线程,不认识 Goroutine。谁来负责把成千上万个 Goroutine 分配给 CPU 跑呢? 这就需要一个"中间商" —— Go 运行时调度器 (Scheduler)。 图示: Thread 与 Goroutine 的区别 2. GMP 模型大揭秘 GMP 其实是三个角色的缩写: G (Goroutine):我们写的代码任务,也就是协程。 M (Machine):工作线程(Thread),对应操作系统的真实线程。它是真正的干活人(搬砖工)。 P (Processor):逻辑处理器(Context),可以理解为"调度上下文"或"资源"。它是包工头,负责管理 G,并把 G 交给 M 去执行。 形象的比喻 想象一个大型搬砖工地: G (砖头):待搬运的任务。 M (工人):负责搬砖的劳动力。 P (手推车):工人必须推着车才能搬砖(因为车里装着搬砖工具和任务清单)。 如果没有 P(手推车),M(工人)就不知道该干啥。 ...

2025-11-28 · 2 min · 364 words · 老墨

[GoLang避坑实战-18] 实战 CLI 工具:30 分钟用 urfave/cli 构建你的极客武器

大家好,我是极客老墨。 上一篇我们写了个小工具——一个简单的命令行计算器,需要从命令行读参数, 我们直接通过 os.Args 来解析命令行参数,这种方式太底层、太原始了,非常容易出错。 说实话,Go 虽然内置了 flag 包,但用起来仍然不是很方便。想要做个像样的 CLI 工具,光靠 flag 远远不够。 今天我们就来聊聊如何用 Go 社区最受欢迎的 urfave/cli 框架,打造真正的极客工具。 为什么不用 flag 包? Go 标准库的 flag 包确实能解析参数,但问题是: 1// 用 flag 包写个简单的工具 2var name = flag.String("name", "", "your name") 3var age = flag.Int("age", 0, "your age") 4 5flag.Parse() 6 7if *name == "" { 8 fmt.Println("name is required") 9 os.Exit(1) 10} 看到没?每个参数都要手动验证,帮助信息要自己拼,子命令?不存在的,得自己实现。 写个小工具还行,但要做个像 git、docker 那样的专业 CLI,光靠 flag 真的会疯。 CLI 框架选择 Go 生态里有几个流行的 CLI 框架: cobra:Kubernetes、Hugo 都在用,功能强大但有点重 urfave/cli:轻量、简单、够用,老墨的首选 survey:专门做交互式 CLI 的,适合问答式工具 今天主要聊 urfave/cli,因为它: ...

2025-11-20 · 7 min · 1448 words · 老墨

[GoLang避坑实战-17] 从零撸一个计算器:基础知识的大合练

大家好,我是极客老墨。 学了这么多语法,是时候写个完整项目了。光看代码片段,很难理解 Go 项目是怎么组织的。这篇我们从零开始,写一个命令行工具,看看真实项目的结构和开发流程。 项目目标 我们要做一个简单的命令行计算器,支持基本的数学运算。 功能需求 支持加减乘除运算 命令行参数输入 彩色输出结果 错误处理 技术要点 Go Modules 依赖管理 标准项目结构 包的导入和使用 第三方库集成 交叉编译 初始化项目 第一步是创建项目目录并初始化模块。 创建项目 1cd go-tutorial-code 2# 创建项目目录 3mkdir 15-project-example 4cd 15-project-example 5 6# 初始化 Go Module 7go mod init github.com/hankmor/calc 这里老墨为了教程的需要,把代码放到了 go-tutorial-code, 并且模块名称没有与文件夹名称一致。 生成的 go.mod: 1module github.com/hankmor/calc 2 3go 1.24 要点: 模块名通常是代码仓库地址 go.mod 是项目的起点 初始化后就可以开始写代码了 项目结构 Go 项目有约定俗成的目录结构。 标准布局 15-project-example/ ├── go.mod # 模块定义 ├── go.sum # 依赖校验 ├── cmd/ # 命令行工具目录 │ └── calc/ │ └── main.go ├── pkg/ # 可导出的库代码 │ └── calculator/ │ ├── calculator.go │ └── calculator_test.go ├── internal/ # 私有代码(不可被外部导入) │ └── utils/ │ └── helper.go ├── README.md # 项目说明 └── Makefile # 构建脚本(可选) 目录说明: ...

2025-11-10 · 8 min · 1512 words · 老墨

[GoLang避坑实战-16] "干净利落!手把手带你搭一套企业级 Go 项目目录

大家好,我是极客老墨。 好的项目结构能让代码更易理解、易维护、易扩展。Go 社区有一套被广泛认可的标准布局,这篇就聊聊如何组织 Go 项目,让代码结构清晰合理。 1. 标准项目布局 参考:golang-standards/project-layout 1myproject/ 2├── cmd/ # 主程序入口 3│ ├── server/ 4│ │ └── main.go # 服务端入口 5│ └── cli/ 6│ └── main.go # 命令行工具入口 7├── internal/ # 私有代码(不可被外部导入) 8│ ├── handler/ 9│ ├── service/ 10│ └── repository/ 11├── pkg/ # 公共库(可被外部导入) 12│ ├── util/ 13│ └── validator/ 14├── api/ # API 定义(OpenAPI/Swagger) 15│ └── openapi.yaml 16├── web/ # Web 静态资源 17│ ├── static/ 18│ └── templates/ 19├── configs/ # 配置文件 20│ ├── config.yaml 21│ └── config.prod.yaml 22├── scripts/ # 脚本(构建、部署等) 23│ ├── build.sh 24│ └── deploy.sh 25├── test/ # 额外的测试数据 26│ └── fixtures/ 27├── docs/ # 文档 28│ └── API.md 29├── go.mod 30├── go.sum 31├── Makefile # 构建脚本 32├── Dockerfile 33└── README.md 2. 核心目录详解 2.1 cmd/ 存放主程序入口,每个子目录对应一个可执行文件。 ...

2025-10-29 · 8 min · 1597 words · 老墨

环境变量与配置管理

大家好,我是极客老墨。 同一份代码需要在开发、测试、生产等不同环境运行。配置管理让我们能够灵活切换环境,而不需要修改代码。这篇就聊聊 Go 的配置管理,从最简单的环境变量到强大的 Viper 库。 1. 环境变量基础 1.1 读取环境变量 1import ( 2 "fmt" 3 "os" 4) 5 6func main() { 7 // 读取环境变量 8 dbHost := os.Getenv("DB_HOST") 9 if dbHost == "" { 10 dbHost = "localhost" // 默认值 11 } 12 13 fmt.Println("DB Host:", dbHost) 14 15 // 检查环境变量是否存在 16 port, exists := os.LookupEnv("PORT") 17 if !exists { 18 port = "8080" 19 } 20} 1.2 设置环境变量 1// 在程序中设置(仅影响当前进程) 2os.Setenv("API_KEY", "secret123") 3 4// 在 shell 中设置 5// export DB_HOST=localhost 6// export DB_PORT=3306 2. godotenv:.env 文件 2.1 安装 1go get -u github.com/joho/godotenv 2.2 使用 创建 .env 文件: ...

2025-09-11 · 4 min · 670 words · 老墨

模糊测试入门 (Fuzzing)

大家好,我是极客老墨。 单元测试只能测试你想到的情况。空字符串、负数、超长输入,这些边界情况很容易遗漏。模糊测试能自动生成大量随机输入,帮你发现那些没想到的 Bug。 这篇就聊聊 Go 的模糊测试,看看它是怎么帮我们提升代码质量的。 什么是模糊测试 模糊测试(Fuzzing)是一种自动化测试技术,通过生成随机输入来发现程序的异常。 核心思想 1// 传统单元测试:测试已知输入 2func TestAdd(t *testing.T) { 3 if Add(2, 3) != 5 { 4 t.Error("2 + 3 should be 5") 5 } 6 if Add(0, 0) != 0 { 7 t.Error("0 + 0 should be 0") 8 } 9} 10 11// 模糊测试:测试大量随机输入 12func FuzzAdd(f *testing.F) { 13 f.Fuzz(func(t *testing.T, a, b int) { 14 result := Add(a, b) 15 // 检查属性而不是具体值 16 if a > 0 && b > 0 && result <= 0 { 17 t.Errorf("Add(%d, %d) = %d, overflow?", a, b, result) 18 } 19 }) 20} 区别: 单元测试:测试已知的输入和输出 模糊测试:测试未知的输入,检查程序属性 能发现什么问题 崩溃和 panic:空指针、数组越界 整数溢出:加法、乘法溢出 无限循环:特定输入导致死循环 内存泄漏:特定输入导致内存不释放 逻辑错误:边界条件处理不当 编写第一个 Fuzz 测试 从一个简单的例子开始。 被测函数 1// reverse.go 2package stringutil 3 4// Reverse 反转字符串 5func Reverse(s string) string { 6 b := []byte(s) 7 for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { 8 b[i], b[j] = b[j], b[i] 9 } 10 return string(b) 11} Fuzz 测试 1// reverse_test.go 2package stringutil 3 4import "testing" 5 6func FuzzReverse(f *testing.F) { 7 // 添加种子输入(可选) 8 f.Add("hello") 9 f.Add("世界") 10 f.Add("") 11 12 // Fuzz 函数 13 f.Fuzz(func(t *testing.T, s string) { 14 // 属性:反转两次应该等于原字符串 15 reversed := Reverse(s) 16 doubleReversed := Reverse(reversed) 17 18 if s != doubleReversed { 19 t.Errorf("Reverse(Reverse(%q)) = %q, want %q", 20 s, doubleReversed, s) 21 } 22 }) 23} 要点: ...

2025-08-20 · 8 min · 1540 words · 老墨

日志管理:从 log 到 zap

大家好,我是极客老墨。 排查问题时,日志是第一手段。服务挂了、请求慢了、数据错了,都要靠日志定位。但日志写不好,要么找不到关键信息,要么性能拖累整个系统。 这篇就聊聊 Go 的日志管理,从标准库到高性能方案,看看怎么写好日志。 标准库 log Go 自带的 log 包,简单但功能有限。 基本使用 1package main 2 3import ( 4 "log" 5) 6 7func main() { 8 // 基本输出 9 log.Println("Server started") 10 11 // 格式化输出 12 log.Printf("User %s logged in", "admin") 13 14 // 带前缀 15 log.SetPrefix("[MyApp] ") 16 log.Println("With prefix") 17 18 // 输出:[MyApp] 2025/07/18 10:00:00 With prefix 19} 要点: log.Println 输出日志 log.Printf 格式化输出 log.SetPrefix 设置前缀 默认输出到 stderr 自定义 Logger 1import ( 2 "log" 3 "os" 4) 5 6func main() { 7 // 创建自定义 Logger 8 logger := log.New( 9 os.Stdout, // 输出目标 10 "[INFO] ", // 前缀 11 log.Ldate|log.Ltime|log.Lshortfile, // 标志 12 ) 13 14 logger.Println("Custom logger message") 15 // 输出:[INFO] 2025/07/18 10:00:00 main.go:15: Custom logger message 16} 标志选项: ...

2025-07-18 · 7 min · 1425 words · 老墨

[GoLang避坑实战-11] 万物皆 Reader:像老鸟一样玩转 Go 语言的 IO 流

大家好,我是极客老墨。 在很多语言里,文件操作和网络操作是两套完全不同的 API。但在 Go 语言里,无论你是读文件、读网络请求,还是读一段内存字符串,你面对的通常都是同一个东西:io.Reader。 这种**“万物皆 Reader”**的设计哲学,是 Go 语言简洁高效的灵魂所在。这篇我们就来深度拆解 Go 的 IO 体系,让你不仅会用,还能写出高性能的 IO 代码。 1. 核心基石:Reader 与 Writer 接口 Go 的 IO 体系建立在两个极简的接口之上: 1type Reader interface { 2 Read(p []byte) (n int, err error) 3} 4 5type Writer interface { 6 Write(p []byte) (n int, err error) 7} 为什么这两个接口牛逼? 因为它屏蔽了底层实现。只要一个类型实现了 Read 方法,它就是 Reader。你可以把一个“读取文件”的 Reader 直接传给一个“处理 HTTP 响应”的函数。 老墨避坑指南: Read 方法里有个细节——io.EOF。它表示“文件结束”,虽然它是一个 error 类型,但在逻辑处理中,它通常标志着读取成功的终点,而不是程序出错了。所以处理时要先看 n > 0 处理数据,再看 err == io.EOF 退出循环。 2. 基础操作:从一次性到流式 2.1 小文件:一键读写 如果你处理的是几 MB 以内的配置文件,os.ReadFile 和 os.WriteFile 是最爽的: 1data, err := os.ReadFile("config.yaml") // 一次性进内存 2if err != nil { /* 处理错误 */ } 3 4err = os.WriteFile("backup.yaml", data, 0644) 2.2 大文件:流式读取(避开 OOM) 处理 GB 级别的日志,你绝不能一次性读取。得用“流”的方式: ...

2025-06-19 · 2 min · 371 words · 老墨

[GoLang避坑实战-15] Go Module 救命指南:彻底告别依赖冲突和环境混乱

大家好,我是极客老墨。 Go 1.11 之前,依赖管理是个大麻烦。GOPATH 要求所有代码放在固定目录,vendor 目录管理混乱,dep 工具又不够成熟。Go Modules 的出现彻底解决了这些问题,现在已经是官方标准方案。 这篇就聊聊 Go Modules,看看它是怎么管理依赖的。 Go Modules 是什么 Go Modules 是 Go 的依赖管理系统,解决了三个核心问题。 核心功能 1// go.mod 文件示例 2module github.com/username/myproject 3 4go 1.21 5 6require ( 7 github.com/gin-gonic/gin v1.9.1 8 gorm.io/gorm v1.25.5 9) 解决的问题: 版本管理:明确指定每个依赖的版本 可重现构建:不同环境构建结果一致 依赖隔离:不同项目可以使用同一个包的不同版本 与 GOPATH 的区别 1# GOPATH 时代(痛苦) 2export GOPATH=$HOME/go 3cd $GOPATH/src/github.com/username/project 4# 所有项目共享依赖,版本冲突频繁 5 6# Go Modules 时代(简单) 7mkdir myproject 8cd myproject 9go mod init github.com/username/myproject 10# 每个项目独立管理依赖 初始化模块 创建新项目时,第一步就是初始化模块。 ...

2025-06-06 · 9 min · 1878 words · 老墨

GoLang教程——泛型编程入门

大家好,我是极客老墨。 Go 1.18 之前,写一个通用的 Min 函数很麻烦。想支持 int 和 float64?要么写两遍代码,要么用 interface{} 加反射,性能还差。其他语言早就有泛型了,Go 终于在 1.18 补上了这个短板。 这篇就聊聊 Go 的泛型,看看它是怎么让代码更通用、更安全的。 为什么需要泛型 没有泛型时,写通用代码很痛苦。 问题:重复代码 1// 比较两个 int 2func MinInt(a, b int) int { 3 if a < b { 4 return a 5 } 6 return b 7} 8 9// 比较两个 float64(重复代码) 10func MinFloat64(a, b float64) float64 { 11 if a < b { 12 return a 13 } 14 return b 15} 16 17// 比较两个 string(又是重复) 18func MinString(a, b string) string { 19 if a < b { 20 return a 21 } 22 return b 23} 问题:interface{} 不安全 1// 使用 interface{} 可以接受任何类型 2func Min(a, b interface{}) interface{} { 3 // 需要类型断言,容易出错 4 // 而且失去了类型安全 5} 解决方案:泛型 1// 一个函数支持多种类型 2func Min[T int | float64 | string](a, b T) T { 3 if a < b { 4 return a 5 } 6 return b 7} 泛型函数 泛型函数使用类型参数,可以处理多种类型。 ...

2025-03-22 · 9 min · 1814 words · 老墨

GoLang教程——Context上下文实战

大家好,我是极客老墨。 写并发程序时,经常遇到这样的场景:用户关闭了浏览器,但后台的数据库查询还在跑;API 调用超时了,但 Goroutine 还在等待响应。这些"失控"的 Goroutine 会浪费资源,甚至导致内存泄漏。 Go 的 Context 就是用来解决这个问题的。它能控制 Goroutine 的生命周期,实现超时、取消和数据传递。 这篇就聊聊 Context 的核心用法,看看它是怎么管理并发任务的。 Context 是什么 Context 是一个接口,定义了四个方法: 1type Context interface { 2 Deadline() (deadline time.Time, ok bool) 3 Done() <-chan struct{} 4 Err() error 5 Value(key interface{}) interface{} 6} 核心功能: 取消信号:通知 Goroutine 停止工作 超时控制:限制任务执行时间 数据传递:在调用链中传递元数据 创建 Context Go 提供了几个函数来创建 Context。 Background 和 TODO 1import "context" 2 3// Background:根 Context,通常在 main 函数中使用 4ctx := context.Background() 5 6// TODO:当不确定用什么 Context 时使用 7ctx := context.TODO() 要点: Background 是最顶层的 Context TODO 用于占位,表示还没想好用什么 两者都不会被取消,没有超时,没有值 WithCancel:手动取消 1// 创建可取消的 Context 2ctx, cancel := context.WithCancel(context.Background()) 3 4// 调用 cancel 取消 Context 5cancel() WithTimeout:超时自动取消 1// 2 秒后自动取消 2ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 3defer cancel() WithDeadline:指定截止时间 1// 指定截止时间 2deadline := time.Now().Add(2 * time.Second) 3ctx, cancel := context.WithDeadline(context.Background(), deadline) 4defer cancel() WithValue:传递数据 1// 存储键值对 2ctx := context.WithValue(context.Background(), "userID", 123) 3 4// 获取值 5if userID, ok := ctx.Value("userID").(int); ok { 6 fmt.Println("User ID:", userID) 7} 超时控制 超时控制是 Context 最常用的场景。 ...

2025-03-02 · 7 min · 1481 words · 老墨

[GoLang避坑实战-14] 写得爽,跑得快:表格驱动测试的工程级避坑实战

大家好,我是极客老墨! 刚转 Go 的时候,我还在找"Go 版的 JUnit 在哪"。结果发现,Go 根本不需要第三方测试框架,go test 命令 + testing 包就够了。 写测试不只是为了完成 KPI,更是为了**“让自己晚上能睡个好觉”**。这篇我们不仅聊基础,更要聊聊在大厂工程实践中,如何写出既稳健又专业的测试。 Go中的测试,不是"一等公民",是"超等公民"。Go 编译器自带 go test 工具,标准库提供 testing 包,写测试简单到爆。 1. 单元测试:从入门到专业 Go 的测试文件规则很简单:以 _test.go 结尾,函数名以 Test 开头。 1.1 基础写法 1func TestAdd(t *testing.T) { 2 if got := Add(1, 2); got != 3 { 3 t.Errorf("Add(1, 2) = %d; want 3", got) 4 } 5} 跑一下: 1$ go test -v 2=== RUN TestAdd 3--- PASS: TestAdd (0.00s) 4PASS 5ok example.com/math 0.392s 就这么简单。不需要装任何库,不需要配置文件。 1.2 进阶:表格驱动(标配) 这是 Go 社区的灵魂写法。把数据和逻辑分离,增加测试用例只需在切片里加一行,这就是“表格驱动”。 1func TestAddTableDriven(t *testing.T) { 2 // 1. 定义测试用例 3 tests := []struct { 4 name string // 用例名称 5 a, b int // 输入 6 want int // 期望结果 7 }{ 8 {"正数", 1, 2, 3}, 9 {"负数", -1, -2, -3}, 10 {"混合", -1, 1, 0}, 11 {"零", 0, 0, 0}, 12 } 13 14 // 2. 遍历执行 15 for _, tt := range tests { 16 // t.Run 启动子测试,方便定位错误 17 t.Run(tt.name, func(t *testing.T) { 18 got := Add(tt.a, tt.b) 19 if got != tt.want { 20 t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want) 21 } 22 }) 23 } 24} 为什么推荐表格驱动? ...

2025-02-19 · 3 min · 491 words · 老墨

[GoLang避坑实战-13] 标准库挖矿:这几个库包能让你少写一半代码

大家好,我是极客老墨。 Go 标准库是个宝藏。fmt 格式化输出、strings 字符串处理、time 时间操作、json 序列化、os 文件读写,这些日常开发 80% 的需求都能搞定,不用到处找第三方库。 今天精选几个最常用的包,看看它们怎么用,能解决什么问题。 fmt:格式化输入输出 fmt 包用于格式化输出和输入,是最常用的包之一。 基本输出 1import "fmt" 2 3func main() { 4 // 输出不换行 5 fmt.Print("Hello") 6 fmt.Print("World") 7 8 // 输出换行 9 fmt.Println("Hello") 10 fmt.Println("World") 11 12 // 格式化输出 13 name := "Go" 14 age := 15 15 fmt.Printf("Name: %s, Age: %d\n", name, age) 16} 常用占位符 1// 字符串 2fmt.Printf("%s\n", "hello") // hello 3fmt.Printf("%q\n", "hello") // "hello" (带引号) 4 5// 整数 6fmt.Printf("%d\n", 42) // 42 (十进制) 7fmt.Printf("%b\n", 42) // 101010 (二进制) 8fmt.Printf("%x\n", 42) // 2a (十六进制) 9 10// 浮点数 11fmt.Printf("%f\n", 3.14) // 3.140000 12fmt.Printf("%.2f\n", 3.14159) // 3.14 (保留2位) 13 14// 布尔值 15fmt.Printf("%t\n", true) // true 16 17// 通用 18fmt.Printf("%v\n", 42) // 42 (默认格式) 19fmt.Printf("%+v\n", struct{X int}{42}) // {X:42} (带字段名) 20fmt.Printf("%#v\n", []int{1,2}) // []int{1, 2} (Go语法) 21fmt.Printf("%T\n", 42) // int (类型) 格式化字符串 1// Sprintf 返回字符串 2msg := fmt.Sprintf("Hello, %s!", "World") 3fmt.Println(msg) // Hello, World! 4 5// Errorf 创建错误 6err := fmt.Errorf("failed to open file: %s", "config.json") 要点: ...

2025-01-12 · 14 min · 2878 words · 老墨

GoLang教程——并发进阶

大家好,我是极客老墨。 并发编程中,Channel 很好用,但不是万能的。有时候需要更精细的控制:等待一组任务完成、保护共享数据、限制并发数量。这时候就需要 sync 包的同步工具了。 这篇就聊聊 Go 的并发进阶工具,看看它们各自适合什么场景。 WaitGroup:等待组 WaitGroup 用于等待一组 Goroutine 完成,是最常用的同步工具。 基本用法 1import ( 2 "fmt" 3 "sync" 4 "time" 5) 6 7func worker(id int, wg *sync.WaitGroup) { 8 defer wg.Done() // 完成时调用 9 10 fmt.Printf("Worker %d starting\n", id) 11 time.Sleep(time.Second) 12 fmt.Printf("Worker %d done\n", id) 13} 14 15func main() { 16 var wg sync.WaitGroup 17 18 for i := 1; i <= 5; i++ { 19 wg.Add(1) // 增加计数 20 go worker(i, &wg) 21 } 22 23 wg.Wait() // 等待所有完成 24 fmt.Println("All workers completed") 25} 要点: Add(n) 增加计数器 Done() 减少计数器(等价于 Add(-1)) Wait() 阻塞直到计数器为 0 必须传递指针 *sync.WaitGroup 常见错误 1// ❌ 错误:在 goroutine 内部 Add 2go func() { 3 wg.Add(1) // 可能在 Wait 之后执行 4 defer wg.Done() 5 // ... 6}() 7 8// ✅ 正确:在启动前 Add 9wg.Add(1) 10go func() { 11 defer wg.Done() 12 // ... 13}() 批量添加 1func main() { 2 var wg sync.WaitGroup 3 4 // 一次性添加多个 5 wg.Add(5) 6 7 for i := 1; i <= 5; i++ { 8 go func(id int) { 9 defer wg.Done() 10 fmt.Printf("Task %d\n", id) 11 }(i) 12 } 13 14 wg.Wait() 15} Mutex:互斥锁 Mutex 用于保护共享数据,确保同一时间只有一个 Goroutine 访问。 ...

2024-12-05 · 9 min · 1714 words · 老墨

[GoLang避坑实战-12] 并发初体验:Goroutine 和 Channel 真的那么神吗?

大家好,我是极客老墨。 传统语言里写并发,要创建线程、加锁、处理竞态条件,一不小心就死锁。Go 的并发模型完全不同:用 Goroutine 代替线程,用 Channel 代替锁。这种 CSP(通信顺序进程)模型,让并发编程变得简单多了。 这篇就聊聊 Go 的并发基础,看看 Goroutine 和 Channel 是怎么配合工作的。 Goroutine 基础 Goroutine 是 Go 的轻量级协程,比线程轻量得多。 启动 Goroutine 使用 go 关键字启动一个 Goroutine。 1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8func sayHello() { 9 fmt.Println("Hello from goroutine") 10} 11 12func main() { 13 // 启动一个 Goroutine 14 go sayHello() 15 16 // 主 Goroutine 继续执行 17 fmt.Println("Hello from main") 18 19 // 等待一下,否则程序会立即退出 20 time.Sleep(time.Second) 21} 要点: go funcName() 启动一个新的 Goroutine Goroutine 是并发执行的,不会阻塞主程序 main 函数结束时,所有 Goroutine 都会被强制终止 Goroutine 的特点 1func printNumbers() { 2 for i := 1; i <= 5; i++ { 3 fmt.Printf("%d ", i) 4 time.Sleep(100 * time.Millisecond) 5 } 6} 7 8func printLetters() { 9 for i := 'A'; i <= 'E'; i++ { 10 fmt.Printf("%c ", i) 11 time.Sleep(100 * time.Millisecond) 12 } 13} 14 15func main() { 16 go printNumbers() 17 go printLetters() 18 19 time.Sleep(time.Second) 20 fmt.Println("\nDone") 21} 输出可能是:1 A 2 B 3 C 4 D 5 E 或其他顺序(不确定) ...

2024-11-19 · 13 min · 2588 words · 老墨

[GoLang避坑实战-10] 不要 panic!Go 风格错误处理的"优雅避坑"指南

大家好,我是极客老墨。 写代码时,错误处理往往占了一半的工作量。文件打不开、网络连不上、数据格式不对,这些都是常态。Go 的错误处理很直接:错误就是一个返回值,你必须显式检查它。 这篇就聊聊 Go 的错误处理机制,看看它是怎么让代码更健壮的。 错误是值 Go 的核心理念:错误是值(Errors are values),不是异常。 error 接口 Go 内置的 error 是一个接口,只有一个方法: 1type error interface { 2 Error() string 3} 任何实现了 Error() string 方法的类型都是 error。 创建错误 1import "errors" 2 3// 方式 1:使用 errors.New 4err1 := errors.New("something went wrong") 5 6// 方式 2:使用 fmt.Errorf(支持格式化) 7err2 := fmt.Errorf("failed to open file: %s", filename) 8 9// 方式 3:自定义错误类型 10type MyError struct { 11 Code int 12 Msg string 13} 14 15func (e MyError) Error() string { 16 return fmt.Sprintf("error %d: %s", e.Code, e.Msg) 17} 要点: ...

2024-10-29 · 10 min · 2103 words · 老墨

[GoLang避坑实战-09] 接口即正义:解密 Go 语言"鸭子类型"的实战精髓

大家好,我是极客老墨。 Go 没有类,也没有继承。它用结构体封装数据,用接口定义行为。这种"组合优于继承"的设计,让代码更灵活、更解耦。 这篇就聊聊 Go 的结构体和接口,看看它们是怎么配合工作的。 结构体基础 结构体是一组字段的集合,用来封装相关的数据。 定义和初始化 1// 定义结构体 2type Person struct { 3 Name string 4 Age int 5} 6 7// 初始化方式 1:按字段顺序 8p1 := Person{"Alice", 30} 9 10// 初始化方式 2:指定字段名(推荐) 11p2 := Person{ 12 Name: "Bob", 13 Age: 25, 14} 15 16// 初始化方式 3:部分字段(其余为零值) 17p3 := Person{Name: "Charlie"} // Age 为 0 18 19// 初始化方式 4:使用 new(返回指针) 20p4 := new(Person) 21p4.Name = "David" 要点: 字段名首字母大写表示导出(public),小写表示私有(package 内可见) 推荐使用字段名初始化,代码更清晰 未初始化的字段会被赋予零值 访问和修改字段 1p := Person{Name: "Alice", Age: 30} 2 3// 访问字段 4fmt.Println(p.Name) // Alice 5 6// 修改字段 7p.Age = 31 8 9// 指针访问(Go 会自动解引用) 10ptr := &p 11ptr.Name = "Alicia" // 等价于 (*ptr).Name = "Alicia" 匿名字段 结构体可以包含匿名字段(只有类型,没有字段名)。 ...

2024-09-15 · 9 min · 1710 words · 老墨

[GoLang避坑实战-08] 切片扩容的惨剧:为什么共享底层数组让你哭都哭不出来?

大家好,我是极客老墨。 写 Go 之前,我在 Java 里用 ArrayList、HashMap 这些集合类。转到 Go 发现只有数组、切片、map 三种,心想"这够用吗?" 结果发现 Go 的切片是动态数组,自动扩容,比 Java 的 ArrayList 还简洁。map 就是哈希表,但遍历顺序是随机的,这点要注意。 这篇就聊聊 Go 数组、切片和 map 的几个关键特性和几大隐蔽却常见的坑。 数组:长度固定,很少用 Go 的数组长度是类型的一部分,[3]int 和 [4]int 是不同类型: 1var arr [3]int = [3]int{10, 20, 30} 2fmt.Println(arr) // [10 20 30] 3 4// 让编译器推导长度 5arr2 := [...]int{1, 2, 3, 4, 5} ⚠️ 注意:数组是值类型,赋值和传参会复制整个数组。 数组的几种初始化方式 1// 指定索引初始化 2arr := [5]int{0: 10, 2: 20, 4: 30} // [10, 0, 20, 0, 30] 3 4// 部分初始化(其余为零值) 5arr2 := [5]int{1, 2} // [1, 2, 0, 0, 0] 6 7// 多维数组 8matrix := [2][3]int{ 9 {1, 2, 3}, 10 {4, 5, 6}, 11} 💡 技巧:实际开发中很少直接用数组,都用切片,因为比起固定长度,我们多数更需要的是动态扩容。 ...

2024-08-22 · 5 min · 1061 words · 老墨

[GoLang避坑实战-07] 扔掉冗长的 if-else:Go 流程控制的"极简之道"

大家好,我是极客老墨。 写 Go 之前,我在 Java 里循环都是 for、while、do-while 三件套。转到 Go 发现只有一个 for,心想"这够用吗?" 结果发现 Go 的 for 能当 while 用,能当无限循环用,还能用 range 遍历。更绝的是 switch 默认不用 break,if 还能带初始化语句。 这篇就聊聊 Go 控制结构的几个巧妙设计。 if:条件判断不用括号 Go 的 if 语句很简洁,条件不需要括号: 1x := 10 2if x > 5 { 3 fmt.Println("x is large") 4} 5 6// if-else 7if x > 10 { 8 fmt.Println("x is large") 9} else { 10 fmt.Println("x is small") 11} ⚠️ 注意:大括号 {} 是必须的,且左大括号不能换行。 初始化语句:限制变量作用域 if 可以带初始化语句,变量作用域仅限于 if-else 块: 1// 常用于错误处理 2if err := doSomething(); err != nil { 3 fmt.Println("Error:", err) 4 return 5} 6 7// 常用于 map 查找 8if value, ok := m["key"]; ok { 9 fmt.Println("Found:", value) 10} else { 11 fmt.Println("Not found") 12} 💡 技巧:这种写法让变量作用域更小,代码更简洁。 ...

2024-08-01 · 5 min · 1027 words · 老墨

[GoLang避坑实战-06] 该选指针接收者吗?老墨带你一次理清函数与方法的纠葛

大家好,我是极客老墨。 函数返回多个值?刚接触 Go 时我还不习惯,写惯了 Java 的单返回值。后来发现这设计真香,错误处理直接 result, err := doSomething(),不用再搞什么异常捕获。 更绝的是 Go 的闭包、defer、方法绑定,这些特性组合起来,让代码既简洁又强大。 这篇就聊聊 Go 函数和方法的几个核心特性。 函数声明:类型在后面 Go 的函数声明用 func 关键字,参数类型写在变量名后面: 1func add(a int, b int) int { 2 return a + b 3} 4 5// 参数类型相同可以合并 6func add(a, b int) int { 7 return a + b 8} 这是 Go 的特色,跟 C/Java 反着来。习惯就好。 多返回值:Go 的杀手锏 Go 支持函数返回多个值,这在错误处理中特别好用: 1func divide(a, b int) (int, error) { 2 if b == 0 { 3 return 0, errors.New("division by zero") 4 } 5 return a / b, nil 6} 7 8// 调用时接收两个返回值 9result, err := divide(10, 0) 10if err != nil { 11 fmt.Println("Error:", err) 12 return 13} 14fmt.Println("Result:", result) 不需要某个返回值?用下划线 _ 忽略: 1result, _ := divide(10, 2) // 忽略错误(不推荐) 命名返回值:可以更简洁 给返回值命名后,可以直接在函数体内使用,还能用裸 return: ...

2024-07-17 · 7 min · 1314 words · 老墨

Golang中singleflight的实现原理

大家好,我是极客老墨。 上一篇我们学习了 singleflight 的用法,知道它可以抑制多个重复请求,极大地节约带宽、提升系统性能。用起来确实爽,但你有没有好奇过:这玩意儿底层到底是怎么实现的?为什么几行代码就能搞定并发控制? 今天我们就来扒一扒 singleflight 的源码,看看它的魔法到底藏在哪里。 核心思路:一个请求干活,其他请求白嫖 singleflight 的核心思路很简单:同一时间段内,对于相同的数据请求,只让第一个请求真正执行,其他请求全部阻塞等待。等第一个请求拿到结果后,直接把结果分享给所有等待的请求。 这就像食堂打饭,第一个人去窗口打饭,后面排队的人都等着。等第一个人打完,大家直接复制他的饭菜,不用再排队了。虽然这个比喻有点扯,但意思就是这么个意思。 回顾一下 singleflight 的公开 API: Group 对象:管理所有请求的大管家 Result 对象:执行结果的包装 Do 方法:同步执行,阻塞等待结果 DoChan 方法:异步执行,通过 channel 返回结果 从这些 API 可以推测:对于同一个 key,首个调用会执行真正的逻辑,后续相同 key 的调用都会阻塞,直到第一个请求返回。 singleflight 的源码不多,算上注释一共就 200 来行。我们来逐一分析。 Group:请求管理的大管家 先看 Group 的定义: 1type Group struct { 2 mu sync.Mutex // protects m 3 m map[string]*call // lazily initialized 4} Group 用一个 map[string]*call 存储所有正在执行的请求。为了保证并发安全,内部持有 sync.Mutex 锁来保护这个 map 的读写。 Group 有两个重要方法 Do 和 DoChan,在上一篇已经介绍过了。 再来回顾一下 Do 方法的定义: 1func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) 参数说明: key:标记同一请求的 key,相同 key 认为是相同请求 fn:真正执行业务逻辑的方法 返回值: ...

2024-06-10 · 4 min · 740 words · 老墨

Golang中singleflight的用法

在开发过程中,尤其是web server,有时候我们需要并发的请求数据,对于某些数据,同时发起的多个请求其实拿到的数据都是相同的,如果不处理这类请求,那么每个请求都会获取一遍数据,这无疑是对资源的浪费。比如要从数据库查询相同 id 的一条数据,并发的多个请求都会执行一次 sql 语句,增加了数据库的压力。 有没有一种方案,当多个请求同时发起后,只有第一个请求去获取数据,在它返回之前,其他请求都各自阻塞等待直到真正执行数据获取的请求返回后,直接拿它的结果?我们把这种将同时发起的多个请求转为一个请求去执行真正业务逻辑的情况称为“请求抑制”。在 Go 中,singleflight 就是用来处理请求抑制的。 简介 singleflight 包全路径为 golang.org/x/sync/singleflight, 目前版本是 v0.7.0。 前边提到,singleflight 用于抑制同一时间获取相同数据的重复请求。当存在多个重复请求时,singleflight 保证只有一个请求能执行,其他请求阻塞,直到前边的请求返回后直接将其返回值共享(shared)给阻塞的这些请求。 在使用之前,首先要理解何为"重复请求",如何区分"相同数据"。 相同数据:指并发下当前时间段多个请求获取的数据是完全相同的,比如获取全局的配置、查询天气数据等。 重复请求:指处理相同数据时,在一个请求从发起到返回之前这段时间,又有其他多个请求发起,那么这些请求就是重复请求。 理解了这两点,现在我们来看看 singleflight 的用法。 用法 singleflight 整体设计比较简单,公开的 api包括: Group 对象: 它表示处理"相同数据"的一系列工作,在这里“重复请求”将会被抑制 Result 对象: 表示执行真正业务逻辑的结果对象 Do 方法: 执行请求抑制,后文详述 DoChan 方法: 与Do相同,只是结果返回 <-chan Result Do 方法 Do 方法表示执行请求抑制,其定义如下: 1func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { 2 // ... 3} 首先,它需要在 singleflight.Group 实例下执行,用于抑制这一组请求。 两个参数: key: 这个参数用来指示"相同数据",也就是说相同的key代表相同数据,区分相同数据是正确使用 singleflight 的关键。 fn: 真正执行业务逻辑的方法,该方法返回一个任意对象 interface{} 和一个 error 返回结果: v: 就是 fn 方法返回的第一个值 err: fn 方法返回的第二值 error shared: 当抑制了其他请求,返回 true, 也就是说将真正执行业务逻辑的请求返回结果共享给其他请求后,该值为 true, 否则为 false DoChan 方法 DoChan 方法与 Do 类似,只是将结果封装为 Result 并返回它的 chan: ...

2024-05-18 · 3 min · 478 words · 老墨

依赖注入库wire使用入门

依赖注入(Dependency injection)不是一个新概念了,早在2004年 Martin Fowler 就在其文章Inversion of Control Containers and the Dependency Injection pattern提出了依赖注入模式。在Java的世界中,随处可见依赖注入的使用,如Spring框架的核心之一就是控制反转(IoC),而依赖注入就是 IoC 思想的一种技术实现,比如我们常用的 Spring 的注解 @Autowire,还有 Jsr250规范定义的 @Resource 注解都实现了依赖注入。 简单而言,依赖注入就是以构建松散耦合的程序为目的、以控制反转为基本思想而在软件工程实现的一种编程技术,它使得程序不需要关注对象的创建逻辑,只是通过函数或者属性告诉程序自己需要什么对象,关注所依赖对象的使用逻辑,从而将对象的创建和使用过程分离开。可见,依赖注入技术遵循依赖倒置原则。 虽然 GO 不是面向对象的语言,但是它也有依赖注入实现,常用的依赖注入框架包括 google 自身出品的 wire、Uber的dig、Facebook的inject等等。本文将介绍 wire 的基本使用。 不使用依赖注入时 在开始介绍 wire 之前,我们来编写一个简单的程序,该程序可以创建问候者并向您问好。 我们创建一个 greeter.go 文件,它包含以下几个结构体: 1type Message string 2 3type Greeter struct { 4 Msg Message 5} 6 7type Event struct { 8 Greeter Greeter 9} Message 表示消息,Greeter 表示问候者,它需要一个 Message,Event 表示一个事件,它用来触发问候。 现在,我们需要增加创建这些结构体的方法: 1func NewMessage(name string) Message { 2 return Message(fmt.Sprintf("hello, %s!", name)) 3} 4 5func NewGreeter(msg Message) Greeter { 6 return Greeter{Msg: msg} 7} 8 9func NewEvent(g Greeter) Event { 10 return Event{Greeter: g} 11} 12 13func (g Greeter) Greet() Message { 14 return g.Msg 15} 16 17func (e Event) Start() { 18 msg := e.Greeter.Greet() 19 fmt.Println(msg) 20} 通过 NewMessage 创建消息,通过 NewGreeter 创建一个问候者,通过 NewEvent 创建事件,然后就可以调用 Start 方法来发起问候了,其实底层最终调用的是 Greeter 的 Greet 方法。 ...

2023-08-06 · 4 min · 680 words · 老墨

(译)Go1.13中处理错误

本文翻译自GoLang官方文档, 见原文地址. 介绍 在过去的十年中,Go 将 “错误作为值” 来处理 ,这对我们很有帮助。尽管标准库对错误的支持很少 —— 只有 errors.New 和 fmt.Errorf 函数,它们产生的错误只包含一条消息 —— 内置 error 接口允许 Go 程序员添加他们想要的任何信息。它所需要的只是一个实现 Error 方法的类型: 1type QueryError struct { 2 Query string 3 Err error 4} 5 6func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() } 像这样的错误类型无处不在,它们存储的信息千差万别,从时间戳到文件名再到服务器地址。通常,该信息包括另一个较低级别的错误以提供额外的上下文。 一个错误包含另一个错误的模式在 Go 代码中非常普遍,经过 广泛讨论 后,Go 1.13 添加了对它的明确支持。这篇文章描述了提供该支持的标准库的新增内容:errors 包中的三个新函数,以及 fmt.Errorf. 在详细描述更改之前,让我们回顾一下在该语言的先前版本中如何检查和构造错误。 Go 1.13 之前的错误 检查错误 Go 错误是值。程序以几种方式根据这些值做出决策。最常见的是比较错误以 nil 查 看操作是否失败。 1if err != nil { 2 // something went wrong 3} 有时我们将错误与已知的 标记 值进行比较,以查看是否发生了特定错误。 1var ErrNotFound = errors.New("not found") 2 3if err == ErrNotFound { 4 // something wasn't found 5} 错误值可以是满足语言定义 error 接口的任何类型。程序可以使用类型断言或类型开关将错误值视为更具体的类型。 ...

2023-05-05 · 4 min · 743 words · 老墨

(译)Go错误是值

Rob Pike,2015 年 1 月 12 日 ,原文地址: https://go.dev/blog/errors-are-values Go 程序员(尤其是刚接触该语言的程序员)讨论的一个共同点是如何处理错误,这些讨论最终往往都将回归于无数次出现的代码片段: 1if err != nil { 2 return err 3} 如上所示,但我们最近扫描了所有我们能找到的开源项目,发现这个片段每页或每两页只出现一次,比某些人认为的要少。尽管如此,许多程序员仍然认为必须键入如下代码来处理错误: 1if err != nil 一直以来,人们觉得一定有什么地方出了问题,而明显的目标就是 Go 本身。 这是不幸的、误导性的,而且很容易纠正。也许刚接触 Go 的程序员会问,“我该如何处理错误?”,然后学习这种模式,最后就此打住。在其他语言中,可能会使用 try-catch 块或其他类似机制来处理错误。因此,程序员认为,我在我的旧语言中可以使用 try-catch 时,但在 go 中我只能输入 if err != nil 来处理错误。随着时间的推移,Go 代码收集了许多这样的片段,结果感觉很笨拙。 不管这种解释是否成立,很明显这些 Go 程序员忽略了关于错误的一个基本观点: 错误是值。 可以对值进行编程,并且由于错误是值,因此可以对错误进行编程。 当然,涉及错误值的常见语句是测试它是否为 nil,但是错误值可以做无数其他事情,应用其中一些其他事情可以使您的程序更好,从而消除大部分样板代码,避免多次使用 if 语句检查每个错误。 一个简单示例是 bufio 包中的 https://go.dev/pkg/bufio/#Scanner[`Scanner`]。它的 https://go.dev/pkg/bufio/#Scanner.Scan[`Scan`] 方法执行底层 I/O,这当然可能导致错误。然而,该 Scan 方法根本不会暴露错误。相反,它返回一个布尔值和一个单独的方法,在扫描结束时运行,报告是否发生错误。客户端代码如下所示: 1scanner := bufio.NewScanner(input) 2for scanner.Scan() { 3 token := scanner.Text() 4 // process token 5} 6if err := scanner.Err(); err != nil { 7 // process the error 8} 当然,有一个 nil 检查错误,但它只出现并执行一次。假设将该 Scan 方法改为: ...

2023-04-30 · 2 min · 389 words · 老墨

(译)Go模块:管理依赖项

文章翻译自Golang官方文档,见原文地址。 当您的代码使用外部包时,这些包(作为模块分发)成为依赖项。随着时间的推移,您可能需要升级或更换它们。Go 提供了依赖管理工具,可帮助您在合并外部依赖项时确保 Go 应用程序的安全。 本文介绍如何执行一些任务来管理您代码中的依赖项,您可以使用 Go tools 执行其中的大部分操作。本主题还介绍了如何执行其他一些您可能会觉得有用的依赖相关任务。 相关阅读: 如果您不熟悉模块和依赖,请查看 入门教程 以获得简要介绍。 使用该 go 命令管理依赖项有助于确保您的需求保持一致,并且您的 go.mod 文件的内容是有效的。有关命令的参考,请参阅 go命令文档。您还可以通过键入 go help 命令名称从命令行获取帮助,如go help mod tidy`. 使用 Go 命令更改依赖项时会编辑 go.mod 文件。有关文件内容的更多信息,请参阅 go.mod 文件参考文档。 让您的编辑器或 IDE 能够感知 Go 模块可以让您更轻松地管理它们。有关支持 Go 的编辑器的更多信息,请参阅 编辑器插件和 IDE。 本主题不描述如何开发、发布和版本模块以供其他人使用。有关更多信息,请参阅 开发和发布模块。 使用和管理依赖项的工作流程 您可以通过 Go tools 获取和使用有用的包。在 pkg.go.dev 上,您可以搜索您觉得有用的包,然后使用 go 命令将这些包导入您自己的代码中以调用它们的功能。 下面列出了最常见的依赖管理步骤: 在 pkg.go.dev 上 «查找和导入包, 查找有用的包»。 «查找和导入包, 在代码中导入所需的包»。 将您的代码添加到模块以进行依赖跟踪(如果它不在模块中)。请参阅 «代码中启用依赖项跟踪, 启用依赖项跟踪» [«添加依赖项, 添加外部包作为依赖项»,以便您可以管理它们。 随着时间的推移,根据需要 «升级或降级依赖项, 升级或降级依赖版本»。 依赖项作为模块管理 在 Go 中,依赖项作为包含导入包的模块来管理。此过程由以下功能支持: ...

2023-04-19 · 3 min · 628 words · 老墨

(译)Rob Pike 带你深入理解 Go 字符串:字节、符文与 Unicode 编码

大家好,我是极客老墨! 今天这篇文章是早期老墨翻译自 Golang 官方博客文章,有一定的深度,写的非常好,读完你会对字符串的设计和底层原理有一个明确的认识,建议收藏后细细品味。 原文地址: https://go.dev/blog/strings, Rob Pike 简介 这篇文章讨论了 Go 中的字符串。起初,字符串对于一篇博文来说似乎太简单了,但要很好地使用它们,不仅需要了解它们的工作原理,还需要了解字节、字符和符文之间的区别,Unicode 和 UTF- 8、字符串和字符串字面量的区别,以及其他更细微的区别。 处理该话题的一种方法首先是回答这个问题:“当我在位置 n 检索 Go 字符串时,为什么我没有得到第 n 个字符?” 正如您将看到的,这个问题引导我们了解有关文本在现代世界中如何工作的许多细节。 什么是字符串? 让我们从一些基础知识开始。 在 Go 中,字符串实际上是只读的字节切片。如果您完全不确定字节切片是什么或它是如何工作的,请阅读 数组、切片和字符串 一文。 重要的是首先要明确一个字符串包含_任意_多个字节,不论字符串是否包含 Unicode 文本、UTF-8 文本或任何其他预定义格式。就字符串的内容而言,它完全等价于一个字节切片([]byte)。 下边是一个字符串(稍后详述),它使用 \xNN 符号来定义一个字符串常量,其中包含一些特殊的字节值(字节的取值范围从十六进制值 00 到 FF)。 const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98" 打印字符串 由于上边我们的示例字符串 sample 中的某些字节不是有效的 ASCII,甚至不是有效的 UTF-8,所以直接打印字符串会产生奇怪的输出。简单的打印语句如下: 1fmt.Println(sample) 产生这种乱码(输出可能因环境而异)输出: ��=� ⌘ 为了找出 sample 字符串底层到底是什么,我们需要把它拆开检查一下。有几种方法可以做到这一点。最明显的是循环其内容并单独提取字节,如以下for循环所示: 1for i := 0; i < len(sample); i++ { 2 fmt.Printf("%x ", sample[i]) // 输出为十六进制格式 3} 正如前文所述,索引字符串访问的是单个字节,而不是单个字符,我们将在下面详细阐述该主题。现在,让我们只使用字节,这是逐字节循环的十六进制输出: bd b2 3d bc 20 e2 8c 98 请注意各个字节如何与定义字符串的十六进制转义匹配。 为凌乱的字符串生成可呈现输出的一种更简便的方法是使用fmt.Printf方法的%x(十六进制)格式,它只是将字符串的每两个连续字节转储为十六进制数字: ...

2023-03-25 · 4 min · 714 words · 老墨

(译)Go创造者Rob Pike带你深入了解数组、切片和字符串 底层的 “append” 原理

大家好,我是极客老墨! 今天这篇文章是早期老墨翻译自 Golang 官方博客文章,有一定的深度,非常经典,读完你会深入了解数组、切片和字符串的 Append 原理,建议收藏后细细品味。 原文地址: https://go.dev/blog/slices,Rob Pike 介绍 过程编程语言最常见的特征之一是数组的概念。数组看起来很简单,但在将它们添加到语言时必须回答许多问题,例如: 固定尺寸还是可变尺寸? 大小是类型的一部分吗? 多维数组是什么样的? 空数组有意义吗? 这些问题的答案会影响数组是否只是语言的一个特性还是其设计的核心部分。 在 Go 的早期开发中,在设计感觉正确之前,花了大约一年的时间来确定这些问题的答案。关键步骤是引入 slices(切片),它建立在固定大小的 array (数组)之上,以提供灵活、可扩展的数据结构。然而,直到今天,刚接触 Go 的程序员经常对切片的工作方式感到困惑,也许是因为其他语言的经验影响了他们的思维。 在这篇文章中,我们将尝试消除混淆。我们将通过构建片段来解释 append 内置函数是如何工作的,以及为什么它会以这种方式工作。 数组 数组是 Go 中的一个重要构建块,但就像建筑物的基础一样,它们通常隐藏在更可见的组件之下。在我们继续讨论更有趣、更强大、更突出的切片概念之前,我们必须简单地讨论一下它们。 数组在 Go 程序中并不常见,因为数组的大小是其类型的一部分,这限制了它的表达能力。 以下代码: 1var buffer [256]byte 声明了一个数组变量 buffer ,[256]byte 表示它持有的数据类型为 byte,长度为 256。如果想声明 512 个字节的数组可以这样: [512]byte。 与数组关联的数据就是 数组中的元素。上边声明的数组缓冲区在内存中看起来像这样: 1buffer: byte byte byte ... 256 times ... byte byte byte 也就是说,该变量只保存 256 个字节的数据,仅此而已。我们可以使用熟悉的索引语法 buffer[0]、buffer[1] 到 buffer[255] 来访问它的元素。(索引范围 0 到 255 涵盖 256 个元素)尝试使用超出此范围的索引值访问 buffer 会使程序崩溃。 内置函数 len 会返回数组、切片或其他一些数据类型中的元素数量。在我们的示例中,len(buffer) 返回固定值 256。 数组有它们适合的使用场景 —— 例如,它们是转换矩阵的良好表示,但它们在 Go 中最常见的用途是为切片保存存储空间。 ...

2023-02-09 · 8 min · 1613 words · 老墨

(译)进入Go模糊测试的世界

原文地址: https://go.dev/doc/tutorial/fuzz[] 本教程介绍了 Go 中模糊测试的基础知识。模糊测试会针对您的测试准备一些随机数据然后运行测试时使用它们,以尝试找出漏洞或导致崩溃的输入。可以通过模糊测试发现的一些漏洞示例包括 SQL 注入、缓冲区溢出、拒绝服务和跨站点脚本攻击(XSS)。 在本教程中,您将为一个简单的函数编写一个模糊测试,运行 go 命令,并调试和修复代码中的问题。 有关本教程中术语的帮助,请参阅 “词汇表”。 您将逐步完成以下部分: «为您的代码创建一个文件夹» «添加代码进行测试» «添加单元测试» «添加模糊测试» «修复无效字符串错误» «修复双反错误» «结论» 注意 更多 Go 教程,请参阅 https://go.dev/doc/tutorial/index.html[教程]。 Go fuzzing 当前支持 https://go.dev/security/fuzz/#requirements[Go Fuzzing 文档] 中列出的内置类型的子集,并支持将来添加的更多内置类型。 先决条件 Go 1.18 或更高版本的安装。 有关安装说明,请参阅 https://go.dev/doc/install[安装 Go]。 用于编辑代码的工具。 您拥有的任何文本编辑器都可以正常工作。 一个命令终端。 Go 在 Linux 和 Mac 上的任何终端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作。 支持模糊测试的环境。 目前仅在 AMD64 和 ARM64 架构上使用覆盖检测进行模糊测试。 [[为您的代码创建一个文件夹]] 为您的代码创建一个文件夹 首先,为您要编写的代码创建一个文件夹。 1、 打开命令提示符并切换到您的主目录。 在 Linux 或 Mac 上: ...

2022-12-19 · 9 min · 1893 words · 老墨

(译)初始Go模糊测试

原文地址: https://go.dev/security/fuzz/ 从 Go 1.18 开始,Go 在其标准工具链中支持模糊测试。Native Go 模糊测试受 https://google.github.io/oss-fuzz/getting-started/new-project-guide/go-lang/#native-go-fuzzing-support[OSS-Fuzz 支持]。 Go模糊测试详细教程见:进入Go模糊测试的世界 一文。 概述 Fuzzing 是一种自动化测试,它不断地操纵程序的输入以查找错误。Go fuzzing 使用覆盖率指导来智能地不断重复执行模糊测试的代码,以发现并向用户报告问题。由于它可以覆盖人类经常错过的边缘情况,因此模糊测试对于发现安全漏洞特别有价值。 下面是一个 «fuzztarget, 模糊测试» 的例子,突出了它的主要组成部分。 上图显示整体模糊测试的示例代码,其中包含一个模糊目标( «corpus, fuzz target» )。 在模糊目标之前调用 f.Add 添加种子语料库,模糊目标的参数高亮显示为fuzzing参数。 编写模糊测试 要求 以下是模糊测试必须遵循的规则。 模糊测试必须是一个形如 FuzzXxx 的函数,以 Fuzz 作为前缀,它只接受一个 *testing.F 参数并且没有返回值。 模糊测试必须在 *_test.go 文件中才能运行。 调用 https://pkg.go.dev/testing#F.Fuzz[(*testing.F).Fuzz] 方法时的匿名函数参数称之为 «fuzztarget, 模糊目标»,形如 func(t *testing.T, xxx)*, 它必须是一个函数,接受一个 *testing.T 作为第一个参数,其他后续参数称为模糊参数,且该函数没有返回值。 每个模糊测试必须只有一个模糊目标。 所有 «seedcoprus, 种子语料库» 条目必须具有与 «fuzzing, 模糊参数» 相同的类型,并且顺序相同。这适用于调用 https://pkg.go.dev/testing#F.Add[(*testing.F).Add] 添加的种子语料库和 testdata/fuzz 目录中已有的语料库文件。 模糊测试参数只能是以下类型: string, []byte int, int8, int16, int32/rune, int64 uint, uint8/byte, uint16, uint32, uint64 float32, float64 bool 建议 以下建议将帮助您充分利用模糊测试。 ...

2022-10-26 · 3 min · 435 words · 老墨

[GoLang避坑实战-05] 玩转 iota:优雅定义常量的黑科技,你真的会用吗?

大家好,我是极客老墨。 写 Go 之前,我在 Java 里定义枚举都是老老实实写 enum。转到 Go 发现没有 enum 关键字,心想"这不得手写 0、1、2、3?" 结果发现 Go 给了个更骚的东西:iota。自动递增,省掉一堆重复代码。 更绝的是 Go 的"无类型常量"设计,直接让你不用管类型转换的破事儿。这篇就聊聊 Go 常量的几个巧妙设计。 常量基础:用 const 声明 常量就是编译期就确定值、运行时不能改的东西。声明很简单: 1const c1 = 1 2const c2 = -100 3const c3 = "hello" 跟变量一样,可以用括号批量声明: 1const ( 2 c4 = 3.14 3 c5 = 1.2 + 12i // Go 支持复数,数学系的福音 4) ⚠️ 小坑:不赋值会复制上一个值 批量声明时,如果某个常量不赋值,它会复制上一个非空表达式的值: 1const ( 2 m = 1 // 1 3 n // 1,复制 m 的值 4 k // 1,继续复制 5 l = m + 1 // 2,新表达式 6 i // 2,复制 l 的表达式结果 7) 8fmt.Println(m, n, k, l, i) // 输出:1 1 1 2 2 这个特性配合 iota 用起来很爽,后面会讲。 Go 的神设计:无类型常量 你注意到没?前面声明常量时都没写类型。这是 Go 的一个巧妙设计:无类型常量。 有类型常量的麻烦 Go 是强类型语言,类型不同就不能直接运算,哪怕底层类型一样: ...

2022-09-10 · 7 min · 1381 words · 老墨

[GoLang避坑实战-04] 变量也玩"躲猫猫"?揭秘 Go 类型系统里那些隐蔽的坑

大家好,我是极客老墨! 从 Java 或 Python 转 Go 的时候,我在变量声明这块卡了好一会儿。 不是说 Go 的变量有多复杂,而是它给了你好几种声明方式。var 和 := 到底啥区别?类型推导怎么玩?指针和 Java 有啥不同?常量还能玩出花来? 这篇文章一次讲透。 变量声明:var 和 := Go 有两种主要声明变量的方式。 用 var 声明 这是最"正统"的写法,适合先声明、后赋值的场景: 1// 声明单个变量 2var name string 3name = "Go" 4 5// 声明时直接赋值(类型可省略) 6var age = 18 // Go 自动推导为 int 7 8// 批量声明 9var ( 10 x int 11 y float64 12 z bool 13 s string 14) 用 := 声明 这是 Go 的语法糖,声明 + 赋值一步到位: 1// 必须初始化,Go 自动推导类型 2message := "Hello, Go!" 3fmt.Println(message) 4 5// 接收函数返回值 6result, err := someFunc() 什么时候用哪个? 用 var 的场景: ...

2022-08-02 · 7 min · 1281 words · 老墨

[GoLang避坑实战-03] 专业选手的养成:用 golangci-lint 直抵企业级代码规范

大家好,我是极客老墨。 Go作为编程语言中的后起之秀,在语言规范上吸收了诸多语言的优点,并形成了自身独特的语言风格。本文探讨一下Go语言的代码风格。 main方法 main 方法同样是go程序的入门函数,但是其定义非常简单,比如上一篇中的: 1func main() { 2 fmt.Println("Hello, world!") 3} 直接用 func 关键字定义一个名为 main 的方法即可,没有任何参数。 那么,如何获取传递给 main 的参数呢,可以使用 os.Args: 1func main() { 2 fmt.Println("hello go!") 3 for i, arg := range os.Args { 4 fmt.Println(i, "=", arg) 5 } 6} os.Args 获取命令行传递的参数,第一个始终为执行程序的名称,一个示例执行结果如下: 1$ go run main_func.go a b 2hello go! 30 = /var/folders/8b/y3pklwbs1wj_cq7hm_yjwgpr0000gn/T/go-build3458283994/b001/exe/main_func 41 = a 52 = b 当然,通常情况不会使用 os.Args 来解析命令行参数,而是使用 go 标准库的 flag 包。 go的关键字 go的内置关键字有25个: 1break default func interface select 2case defer go map struct 3chan else goto package switch 4const fallthrough if range type 5continue for import return var 这25个关键字不能作为标识符使用。 标识符命名 Go中的标识符同java一样,必须由字母、数字和下划线"_“组成,而且第一个不能为数字: 1func main() { 2 _a := 1 3 fmt.Println(_a) 4 b := 2 5 fmt.Println(b) 6 // 3c := 3 // 不能编译 7 你好 := 5 8 println(你好) 9} 有意思的是,除了25个关键字之外的合法标识符,只要不存在冲突,都可以作为标识符,比如包名也可以作为标识符: ...

2022-07-30 · 7 min · 1438 words · 老墨

[GoLang避坑实战-02] 初识 Go:除了高并发,它还有哪些让你尖叫的特性?

大家好,我是极客老墨。这是本教程的第一篇,我们讲介绍 Go 语言的发展历史,并完成安装、运行 HelloWorld 程序。 Go 语言是最近几年发展最快、最火热的编程语言之一,由 Google 公司出品,其学习成本低、天生支持高并发、语法简洁高效、强大的标准库以及日益丰富的生态特性等使得其非常适合开发后端服务程序,并逐渐挑战着 Java 在服务端编程语言中的领导地位。 Go语言的诞生 Go 语言的前身被认为是一种名为 Limbo 的编程语言,它是由 Unix 之父、C语言之父 肯·汤普逊(Ken Thompson) 和 丹尼斯·里奇(Dennis Ritchie) 这两位计算机灵魂人物领衔并在一个名为 Plan 9 的操作系统研究项目中发明的。 Go语言起源于2007年,当时还是身在 Google 的 Plan 9 项目原班人马在业余时间编写完成的,后来 Google 意识到了 Go 的巨大潜力并全力支持这个项目。Go 在2009年11月正式对外开放,并在2012年3月28日发布了第一个正式版本。 Go 语言的主要开发者有 肯·汤普逊(Ken Thompson)、罗布·派克(Rob Pike)、罗伯特·格里泽默(Robert Griesemer)等,每一位都是赫赫有名的大师级人物: 肯·汤普逊:与丹尼斯·里奇在贝尔实验室发明了Unix操作系统,以及用于该系统的 C编程语言 罗布·派克:Unix小组成员,参与了Unix后续的Plan 9和Inferno操作系统,同时也是Limbo语言和Go语言共同的发明者 罗伯特·格里泽默:协助制作Java的HotSpot编译器和Chrome浏览器的JavaScript引擎V8,Go语言的共同创造者 Go的特点 Go 始于 2007 年 9 月,当时 Robert Griesemer、Ken Thompson 和我开始讨论一种新语言,以解决我们和 Google 的同事在日常工作中面临的工程挑战。 ...

2022-07-21 · 5 min · 1061 words · 老墨

(译)Go并发模式——Context

原文地址: https://go.dev/blog/context[] 作者:Sameer Ajmani 时间:29 July 2014 简介 footnote:ddns[这篇文章是go官网博客中的一篇,尽管文章比较早,但是较详细的描述了 context 出现的原因、使用方式,仍然值得一读。] 在 Go 服务器中,每个传入的请求都在其自己的 goroutine 中处理。请求处理程序通常会启动额外的 goroutine 来访问数据库和 RPC 服务等后端。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当请求被取消或超时时,所有处理该请求的 goroutines 都应该快速退出,以便系统可以回收它们正在使用的任何资源。 在 Google,我们开发了一个 context 包,可以轻松地将请求范围的值、取消信号和截止日期等跨 API 边界传递给正在处理请求的所有 goroutine。 https://go.dev/pkg/context[该软件包] 作为context公开可用 。本文介绍了如何使用该包并提供了一个完整的工作示例。 Context context 包的核心是 Context 类型: 1type Context interface { // <1> 2 Done() <-chan struct{} // <2> 3 4 Err() error // <3> 5 6 Deadline() (deadline time.Time, ok bool) // <4> 7 8 Value(key interface{}) interface{} // <5> 9} <1> Context 携带截止日期、取消信号和请求范围的跨越 API 边界的值,多个 goroutine 同时使用它的方法是安全的。 <2> Done 返回一个在此 Context 取消或超时时的通道(chan) <3> Err 错误信息说明 context 为什么被取消, 在 Done 返回的 chan 被关闭之后获取 <4> Deadline 返回 Context 被取消的时间 <5> Value 返回参数 key 关联的值,没有则返回 nil ...

2022-07-19 · 5 min · 1012 words · 老墨

[GoLang避坑实战-01] Go 1.24 安装排雷指南:拒绝被"环境变量"折磨

大家好,我是极客老墨! 在正式开始这套 Golang 基础教程之前,老墨要先跟你聊聊一个很多人容易忽略的问题:环境配置。 你可能会想:“不就是装个 Go 嘛,有啥好说的?” 但老墨见过太多同学,拿着 Go 1.11 的版本跑 Go 1.18 的泛型代码,然后在群里问:“为什么我的代码报错?” 所以,磨刀不误砍柴工,咱们先把环境搞对,后面学起来才能一路畅通。 关于本教程 这套 Golang 教程分为三个部分: 基础教程(本系列):从零开始学习 Go 语言的核心语法和基础特性 高级教程(规划中):工程实践、性能优化、并发模式等进阶内容 Web 开发(规划中):Gin、GORM 等 Web 框架的实战应用 目前你正在阅读的是基础教程部分,适合 Go 语言初学者或想系统学习 Go 基础的开发者。高级教程和 Web 开发部分老墨会在后续完善后陆续发布,敬请期待! 1. Go 版本选择 1.1 本教程使用的版本 本教程所有代码基于 Go 1.24+ 编写和测试。 由于老墨编写这一套教程时间跨度较大,可能教程中的 Go 版本较旧,但是老墨建议大家都使用最新的 LTS 版本即可,GoLang 官方保证了兼容性,所以理论上最新版本的 Go 都可以跑本教程的代码。 1.2 如何查看你的 Go 版本 1go version 如果输出类似这样,就说明安装成功了: go version go1.24.0 darwin/arm64 1.3 版本兼容性说明 Go 版本 本教程兼容性 说明 1.24+ 完全兼容 推荐使用,支持所有最新特性 1.21-1.23 完全兼容 支持泛型、slog 等特性,可以正常使用 1.18-1.20 部分兼容 泛型相关代码可以跑,但 slog 等新特性不可用 1.16-1.17 不兼容 不支持泛型,部分代码无法运行 < 1.16 不兼容 太老了,建议升级 2. 安装 Go 2.1 macOS 安装 方法一:使用 Homebrew(推荐) ...

2022-07-10 · 3 min · 449 words · 老墨