为什么我建议大家要学习Golang

为什么我建议大家学习Golang 大家好,我是极客老墨。 在互联网行业摸爬滚打十几年,从早期的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 · 178 words · 老墨

为了学 AI,我用 Go + Fyne 手撸了一个原生视频下载器

为了学 AI,我用 Go + Fyne 手撸了一个原生视频下载器 大家好,我是老墨。 一个写了十几年代码,发际线依然坚挺,但最近确实有点焦虑的中年程序员。 为啥焦虑?还不是因为这该死的 AI。 从 24 年初 Sora 横空出世,到 Claude 3.5 杀疯了,再到最近 DeepSeek 甚至能自己修 Bug,这世界变化快得像开了二倍速。以前我们卷算法、卷架构,现在倒好,不仅要卷提示词,还要防着被自己的 IDE 抢了饭碗。 老墨我痛定思痛,觉得不能坐以待毙。打不过就加入嘛,我也开始疯狂恶补 AI 知识。 01 一个悲伤的故事:学习资料太多也是一种烦恼 要学 AI,最好的路径是什么?这年头文档更新赶不上模型迭代,最鲜活的知识全在视频里。YouTube 上的 Andrej Karpathy 大神课,B 站上的各种论文精读、实战通过… 于是我的浏览器收藏夹很快就爆了。 但我这人有个毛病,看视频喜欢囤。一来是有些干货太硬,得反复咀嚼;二来是作为技术人,总有一种由于网络不确定性带来的"松鼠症"——好东西必须存在本地硬盘里才踏实。 这时候问题来了:市面上的下载器,怎么就没一个顺手的? 某雷:广告多是其次,关键是我想下的 YouTube 和 B 站视频它基本都解析不了。对于我们这种想看外网 AI 前沿教程的人来说,它形同虚设。 某 IDM:嗅探功能很强,但碰到 YouTube/B 站 这种音视频分离的高画质(DASH流)视频就歇菜了,经常只能下个无声画面,心累。而且 Mac 上没有原生版,体验割裂。 Electron 系工具:界面是好看了,但这动不动几百兆的内存占用,我开个 PyCharm 跑模型本来就捉襟见肘,哪还有余粮养它们? 命令行 yt-dlp:这是真神,功能无敌。用了很久,自己写脚本都写了多个。但时间久了脚本太多,每次想下个视频还得敲命令,复制粘贴 URL,还得拼代理参数… 实在是比较麻烦。 “求人不如求己,为什么不能自己写一个GUI?” 这念头一出,我就兴奋了。写了几年的 Golang,这点事情不在话下。说干就干,开搞开搞。 02 技术选型:要做就做原生的 既然决定自己干,那必须立好 Flag: 要快:启动快,下载快。 要美:虽然我是后端出身,但也不能忍受丑陋的 UI。 要轻:拒绝 Electron,拒绝 WebView,我要纯原生。 基于这个标准,技术栈基本就锁死了: ...

2026-02-04 · 2 min · 360 words · 老墨

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

Go 高级教程:反射 (Reflection) 实战 “反射是魔鬼。” —— 某些性能洁癖者 “没有反射,就没有现代 Web 框架。” —— 现实主义开发者 反射 (Reflection) 赋予了程序在 运行时 (Runtime) 检查和修改自身状态的能力。从 JSON 解析到 ORM 框架(如 GORM),再到依赖注入,它们的底层都离不开反射。 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 · 449 words · 老墨

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

Go 高级教程:其他并发工具 如果说 Goroutine 和 Channel 是 Go 并发的“常规武器”,那么 sync 包里的工具就是“特种装备”。虽然不常用,但关键时刻能救命(榨干 CPU 的最后一点性能)。 除了 这里 介绍的诸多基础并发工具外,Go 标准库还提供了一些高级并发工具,下面介绍几个比较常用的。 1. 减轻 GC 压力:sync.Pool 我们在讲 GC 的时候提过,如果你频繁申请和销毁大对象(比如 HTTP Response 对象,或者大的 byte buffer),GC 会鸭梨山大。 sync.Pool 就是为了对象复用而生的。 1.1 示例代码 1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8// 定义池子 9var bufPool = 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 // 1.Get(): 借一个对象 19 buf := bufPool.Get().([]byte) 20 21 // 用完它... 22 23 // 2. Put(): 还回去,下次给别人用 24 // 注意:还之前最好重置一下状态(比如清空) 25 bufPool.Put(buf) 26 27 // 再次 Get,就不会触发 New,而是直接复用刚才那个 28 buf2 := bufPool.Get().([]byte) 29 _ = buf2 30} 1.2 注意事项 sync.Pool 里的对象随时可能被 GC 回收!所以绝对不要用它存数据库连接、Socket 连接这种必须长久保持的资源。它只适合存“临时垃圾”。 ...

2025-12-20 · 3 min · 475 words · 老墨

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

Go 高级教程:深入理解 GMP 调度器 为什么 Go 语言能轻松支撑百万并发? 为什么 Goroutine 切换成本这么低? 这一切的背后,都站着一位神秘的大管家 —— GMP 调度器。 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 · 289 words · 老墨

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 · 3 min · 596 words · 老墨

环境变量与配置管理

环境变量与配置管理 同一份代码需要在开发、测试、生产等不同环境运行。配置管理让我们能够灵活切换环境,而不需要修改代码。 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 文件: 1DB_HOST=localhost 2DB_PORT=3306 3DB_USER=root 4DB_PASSWORD=secret 5API_KEY=your-api-key 6DEBUG=true 加载配置: ...

2025-09-11 · 3 min · 620 words · 老墨

模糊测试入门 (Fuzzing)

单元测试只能测试你想到的情况,而模糊测试能帮你发现你没想到的边界情况。 1. 什么是模糊测试? 模糊测试 (Fuzzing) 是一种自动化测试技术,通过生成大量随机或半随机的输入数据来测试程序,寻找崩溃、panic、死循环等异常。 传统测试 vs 模糊测试: 1// 传统单元测试:测试已知的输入 2func TestAdd(t *testing.T) { 3 if Add(2, 3) != 5 { 4 t.Error("2 + 3 should be 5") 5 } 6} 7 8// 模糊测试:测试大量随机输入 9func FuzzAdd(f *testing.F) { 10 f.Fuzz(func(t *testing.T, a, b int) { 11 result := Add(a, b) 12 // 检查属性而不是具体值 13 if result < a && result < b { 14 t.Errorf("Add(%d, %d) = %d, should be >= both", a, b, result) 15 } 16 }) 17} 2. 编写 Fuzz 测试 2.1 基础示例 假设我们有一个解析 URL 的函数: 1// url.go 2package myurl 3 4import ( 5 "fmt" 6 "strings" 7) 8 9func ParseURL(rawURL string) (string, error) { 10 if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { 11 return "", fmt.Errorf("invalid protocol") 12 } 13 return rawURL, nil 14} 编写 Fuzz 测试: ...

2025-08-20 · 3 min · 619 words · 老墨

日志管理:从 log 到 zap

日志是排查问题的第一手段。一个好的日志系统应该:结构化、高性能、可配置、易于检索。 1. 标准库 log 1.1 基础使用 1package main 2 3import ( 4 "log" 5) 6 7func main() { 8 log.Println("This is a log message") 9 log.Printf("User %s logged in", "admin") 10 11 // log.Fatal 会调用 os.Exit(1) 12 // log.Fatal("Fatal error") 13 14 // log.Panic 会触发 panic 15 // log.Panic("Panic error") 16} 1.2 自定义 Logger 1import ( 2 "log" 3 "os" 4) 5 6func main() { 7 // 创建自定义 Logger 8 logger := log.New( 9 os.Stdout, // 输出目标 10 "[MyApp] ", // 前缀 11 log.Ldate|log.Ltime|log.Lshortfile, // 标志 12 ) 13 14 logger.Println("Custom logger message") 15 // 输出:[MyApp] 2025/12/18 10:00:00 main.go:15: Custom logger message 16} 1.3 写入文件 1func main() { 2 file, err := os.OpenFile("app.log", 3 os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 4 if err != nil { 5 panic(err) 6 } 7 defer file.Close() 8 9 log.SetOutput(file) 10 log.Println("This goes to file") 11} 2. slog:结构化日志 (Go 1.21+) 2.1 基础使用 1import ( 2 "log/slog" 3 "os" 4) 5 6func main() { 7 // 创建 JSON 格式的 Logger 8 logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 9 10 logger.Info("User logged in", 11 "user_id", 123, 12 "username", "admin", 13 "ip", "192.168.1.1") 14 15 // 输出: 16 // {"time":"2025-12-18T10:00:00Z","level":"INFO","msg":"User logged in","user_id":123,"username":"admin","ip":"192.168.1.1"} 17} 2.2 日志级别 1func main() { 2 logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 3 Level: slog.LevelInfo, // 只输出 Info 及以上级别 4 })) 5 6 logger.Debug("Debug message") // 不会输出 7 logger.Info("Info message") // 会输出 8 logger.Warn("Warning message") // 会输出 9 logger.Error("Error message") // 会输出 10} 2.3 上下文日志 1func main() { 2 logger := slog.Default() 3 4 // 创建带有固定字段的子 Logger 5 requestLogger := logger.With( 6 "request_id", "abc123", 7 "user_id", 456, 8 ) 9 10 requestLogger.Info("Processing request") 11 requestLogger.Info("Request completed") 12 13 // 两条日志都会自动包含 request_id 和 user_id 14} 3. zap:高性能日志库 3.1 安装 1go get -u go.uber.org/zap 3.2 快速开始 1import ( 2 "go.uber.org/zap" 3) 4 5func main() { 6 // 开发环境:易读的格式 7 logger, _ := zap.NewDevelopment() 8 defer logger.Sync() // 刷新缓冲区 9 10 logger.Info("User logged in", 11 zap.String("username", "admin"), 12 zap.Int("user_id", 123)) 13 14 // 生产环境:JSON 格式 15 prodLogger, _ := zap.NewProduction() 16 defer prodLogger.Sync() 17 18 prodLogger.Info("Server started", 19 zap.String("port", "8080")) 20} 3.3 自定义配置 1func main() { 2 config := zap.NewProductionConfig() 3 4 // 设置日志级别 5 config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) 6 7 // 设置输出路径 8 config.OutputPaths = []string{ 9 "stdout", 10 "./logs/app.log", 11 } 12 13 // 设置错误日志路径 14 config.ErrorOutputPaths = []string{ 15 "stderr", 16 "./logs/error.log", 17 } 18 19 logger, _ := config.Build() 20 defer logger.Sync() 21 22 logger.Info("Application started") 23} 3.4 性能对比 1// zap 提供了 SugaredLogger,牺牲一点性能换取更简洁的 API 2logger, _ := zap.NewProduction() 3sugar := logger.Sugar() 4 5// 结构化日志(最快) 6logger.Info("User logged in", 7 zap.String("username", "admin")) 8 9// 格式化日志(稍慢,但更方便) 10sugar.Infof("User %s logged in", "admin") 4. 日志分级 4.1 日志级别 从低到高: ...

2025-07-18 · 3 min · 617 words · 老墨

文件与 IO 操作实战

文件操作是编程中最基础也最常用的需求。Go 语言的 io 包设计优雅,通过 io.Reader 和 io.Writer 接口实现了高度的抽象和复用。 1. 文件读取 1.1 一次性读取整个文件 1package main 2 3import ( 4 "fmt" 5 "os" 6) 7 8func main() { 9 // 方法一:os.ReadFile (推荐,Go 1.16+) 10 data, err := os.ReadFile("test.txt") 11 if err != nil { 12 panic(err) 13 } 14 fmt.Println(string(data)) 15 16 // 方法二:ioutil.ReadFile (已废弃,但仍可用) 17 // data, err := ioutil.ReadFile("test.txt") 18} 1.2 逐行读取(大文件) 1import ( 2 "bufio" 3 "fmt" 4 "os" 5) 6 7func main() { 8 file, err := os.Open("large.txt") 9 if err != nil { 10 panic(err) 11 } 12 defer file.Close() // 确保文件关闭 13 14 scanner := bufio.NewScanner(file) 15 for scanner.Scan() { 16 line := scanner.Text() 17 fmt.Println(line) 18 } 19 20 if err := scanner.Err(); err != nil { 21 panic(err) 22 } 23} 1.3 按块读取 1func main() { 2 file, _ := os.Open("data.bin") 3 defer file.Close() 4 5 buffer := make([]byte, 1024) // 每次读 1KB 6 for { 7 n, err := file.Read(buffer) 8 if err == io.EOF { 9 break // 文件读完 10 } 11 if err != nil { 12 panic(err) 13 } 14 fmt.Printf("Read %d bytes\n", n) 15 // 处理 buffer[:n] 16 } 17} 2. 文件写入 2.1 一次性写入 1func main() { 2 data := []byte("Hello, World!\n") 3 4 // 写入文件(会覆盖原文件) 5 // 0644 是文件权限:rw-r--r-- 6 err := os.WriteFile("output.txt", data, 0644) 7 if err != nil { 8 panic(err) 9 } 10} 2.2 追加写入 1func main() { 2 file, err := os.OpenFile("log.txt", 3 os.O_APPEND|os.O_CREATE|os.O_WRONLY, // 追加模式 4 0644) 5 if err != nil { 6 panic(err) 7 } 8 defer file.Close() 9 10 file.WriteString("New log entry\n") 11} 2.3 使用 bufio 缓冲写入 1func main() { 2 file, _ := os.Create("output.txt") 3 defer file.Close() 4 5 writer := bufio.NewWriter(file) 6 7 for i := 0; i < 1000; i++ { 8 writer.WriteString(fmt.Sprintf("Line %d\n", i)) 9 } 10 11 writer.Flush() // 重要:将缓冲区内容写入文件 12} 3. 文件与目录操作 3.1 检查文件是否存在 1func fileExists(path string) bool { 2 _, err := os.Stat(path) 3 return !os.IsNotExist(err) 4} 3.2 创建目录 1// 创建单层目录 2os.Mkdir("mydir", 0755) 3 4// 创建多层目录(类似 mkdir -p) 5os.MkdirAll("path/to/mydir", 0755) 3.3 遍历目录 1import ( 2 "fmt" 3 "os" 4 "path/filepath" 5) 6 7func main() { 8 // 方法一:filepath.Walk 9 filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 10 if err != nil { 11 return err 12 } 13 if !info.IsDir() { 14 fmt.Println("File:", path) 15 } 16 return nil 17 }) 18 19 // 方法二:os.ReadDir (Go 1.16+) 20 entries, _ := os.ReadDir(".") 21 for _, entry := range entries { 22 fmt.Println(entry.Name(), entry.IsDir()) 23 } 24} 3.4 删除文件/目录 1// 删除文件 2os.Remove("file.txt") 3 4// 删除目录及其所有内容(类似 rm -rf) 5os.RemoveAll("mydir") 4. io.Copy 文件拷贝 4.1 复制文件 1func copyFile(src, dst string) error { 2 source, err := os.Open(src) 3 if err != nil { 4 return err 5 } 6 defer source.Close() 7 8 destination, err := os.Create(dst) 9 if err != nil { 10 return err 11 } 12 defer destination.Close() 13 14 // io.Copy 高效复制 15 _, err = io.Copy(destination, source) 16 return err 17} 4.2 显示进度的复制 1type ProgressReader struct { 2 reader io.Reader 3 total int64 4 read int64 5} 6 7func (pr *ProgressReader) Read(p []byte) (int, error) { 8 n, err := pr.reader.Read(p) 9 pr.read += int64(n) 10 11 // 打印进度 12 fmt.Printf("\rProgress: %.2f%%", float64(pr.read)/float64(pr.total)*100) 13 14 return n, err 15} 5. 网络文件下载 1import ( 2 "io" 3 "net/http" 4 "os" 5) 6 7func downloadFile(url, filepath string) error { 8 // 发起 HTTP GET 请求 9 resp, err := http.Get(url) 10 if err != nil { 11 return err 12 } 13 defer resp.Body.Close() 14 15 // 创建文件 16 out, err := os.Create(filepath) 17 if err != nil { 18 return err 19 } 20 defer out.Close() 21 22 // 将响应体复制到文件 23 _, err = io.Copy(out, resp.Body) 24 return err 25} 26 27func main() { 28 downloadFile("https://example.com/file.zip", "file.zip") 29} 6. io.Reader 和 io.Writer 接口 6.1 理解接口 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} 实现了这两个接口的类型: ...

2025-06-19 · 4 min · 782 words · 老墨

Go 模块管理入门

在 Go 1.11 之前,依赖管理是 Go 语言的一大痛点。GOPATH、vendor、dep 等方案各有缺陷。Go Modules 的出现彻底解决了这个问题,成为 Go 官方推荐的依赖管理方案。 1. 什么是 Go Modules? Go Modules 是 Go 语言的依赖管理系统,它解决了以下问题: 版本管理:明确指定依赖的版本 可重现构建:确保不同环境构建结果一致 依赖隔离:不同项目可以使用同一个包的不同版本 2. 初始化模块 2.1 创建新模块 1# 创建项目目录 2mkdir myproject 3cd myproject 4 5# 初始化模块 6# 模块路径通常是代码仓库地址 7go mod init github.com/username/myproject 这会生成 go.mod 文件: 1module github.com/username/myproject 2 3go 1.21 2.2 go.mod 文件结构 1module github.com/username/myproject // 模块路径 2 3go 1.21 // Go 版本 4 5require ( 6 github.com/gin-gonic/gin v1.9.1 // 直接依赖 7 gorm.io/gorm v1.25.5 8) 9 10require ( 11 github.com/gin-contrib/sse v0.1.0 // 间接依赖(由 gin 引入) 12 // ... 更多间接依赖 13) // indirect 14 15exclude ( 16 github.com/some/package v1.2.3 // 排除某个版本 17) 18 19replace ( 20 github.com/old/package => github.com/new/package v1.0.0 // 替换依赖 21) 3. 常用命令 3.1 添加依赖 1# 方法一:直接在代码中 import,然后运行 2go mod tidy 3 4# 方法二:手动添加 5go get github.com/gin-gonic/gin@v1.9.1 6 7# 获取最新版本 8go get github.com/gin-gonic/gin@latest 9 10# 获取特定版本 11go get github.com/gin-gonic/gin@v1.8.0 12 13# 获取某个 commit 14go get github.com/gin-gonic/gin@abc1234 3.2 go mod tidy 最常用的命令,它会: ...

2025-06-06 · 3 min · 461 words · 老墨

GoLang教程——项目实战示例

学习了基础语法后,是时候动手写一个完整的项目了。本章将从零开始构建一个简单的 CLI 工具(简易计算器),重点讲解 Go Modules 依赖管理和标准的 项目目录结构。 1. 初始化项目 首先,创建一个文件夹并初始化 Go Module。go mod 是 Go 官方的依赖管理工具。 1mkdir my-calc 2cd my-calc 3# 初始化模块,模块名为 example.com/calc 4go mod init example.com/calc 执行后会生成一个 go.mod 文件,它声明了模块名称和 Go 版本。 2. 项目结构 虽然 Go 对目录结构没有强制要求,但社区约定俗成的标准布局(Standard Layout)如下: 1my-calc/ 2├── go.mod // 模块定义 3├── main.go // 程序入口 4└── pkg/ // 库代码目录 5 └── mathop/ // 子包:数学操作 6 └── add.go 3. 编写代码 编写子包逻辑 在 pkg/mathop/add.go 中编写核心业务逻辑: 1package mathop 2 3// Add 函数首字母大写,表示是可以导出的(Public) 4func Add(a, b int) int { 5 return a + b 6} 编写入口函数 在根目录 main.go 中调用我们的子包: ...

2025-05-25 · 2 min · 228 words · 老墨

GoLang教程——泛型编程入门

在 Go 1.18 之前,由于缺乏泛型,我们想编写一个通用的 Min 函数(既能比对 int 也能比对 float),通常需要写两遍代码,或者使用性能较差的 interface{} 和反射。 Go 1.18 终于引入了 泛型 (Generics),允许我们在函数和类型定义中使用 类型参数 (Type Parameters)。这极大地提高了代码的复用性和类型安全性。 1. 泛型函数 先看一个传统的非泛型版本: 1func MinInt(a, b int) int { 2 if a < b { return a } 3 return b 4} 5// 如果要支持 float,还得再写一个 MinFloat... 使用泛型重写: 1import "fmt" 2 3// [T int | float64] 定义了类型参数 T 4// T 被限制为 int 或 float64(类型约束) 5func Min[T int | float64](a, b T) T { 6 if a < b { 7 return a 8 } 9 return b 10} 11 12func main() { 13 // 显式实例化 14 fmt.Println(Min[int](10, 20)) 15 16 // 隐式类型推导(推荐):编译器自动根据参数推导出 T 是 float64 17 fmt.Println(Min(3.14, 1.59)) 18} 2. 自定义泛型类型 除了函数,结构体也可以是泛型的。比如我们要实现一个通用的栈(Stack)。 ...

2025-03-22 · 2 min · 318 words · 老墨

GoLang教程——Context上下文实战

在 Go 的并发编程中,context(上下文)包是绝对的核心。无论是处理 HTTP 请求、RPC 调用,还是数据库查询,Context 都扮演着控制 Goroutine 生命周期的角色。 简而言之,Context 主要解决三个问题: 取消信号:通知子 Goroutine 停止工作,释放资源。 超时控制:规定任务必须在多长时间内完成,否则强制取消。 数据传递:在调用链中传递请求范围内的元数据(如 UserID, TraceID)。 1. 为什么需要 Context? 假设你启动了一个 Goroutine 去查询数据库,如果用户突然关闭了浏览器,或者请求处理太慢超时了,你希望这個后台查询任务能立即停止,而不是继续浪费数据库资源。这就是 Context 的用武之地。 Go 的设计原则:谁启动了 Goroutine,谁就有责任(通过 Context)管理它的退出。 2. 超时控制 (WithTimeout) 这是 Context 最常用的场景。 1package main 2 3import ( 4 "context" 5 "fmt" 6 "time" 7) 8 9func main() { 10 // 1. 创建一个带超时的 Context 11 // 规定任务必须在 2 秒内完成,否则 ctx 会收到取消信号 12 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 13 // 关键:函数结束时必须调用 cancel,防止内存泄漏 14 defer cancel() 15 16 fmt.Println("Start working...") 17 18 // 2. 将 ctx 传递给耗时任务 19 doSlowWork(ctx) 20} 21 22func doSlowWork(ctx context.Context) { 23 // 模拟一个需要 3 秒才能完成的任务 24 // Select 会等待 done 信号或者 work 完成,以此来实现超时抢占 25 select { 26 case <-time.After(3 * time.Second): // 模拟业务逻辑 27 fmt.Println("Work done successfully!") 28 case <-ctx.Done(): // 监听 Context 的取消信号 29 // 如果超时了,ctx.Err() 会返回 DeadlineExceeded 30 fmt.Println("Work cancelled:", ctx.Err()) 31 } 32} 运行结果: 由于任务耗时 3秒,但 Context 限制 2秒,所以输出: Work cancelled: context deadline exceeded ...

2025-03-02 · 2 min · 359 words · 老墨

GoLang教程——单元测试与基准测试

在 Go 语言中,测试不是“一等公民”,而是“超等公民”。Go 编译器自带了 go test 工具,标准库提供了 testing 包,这使得编写测试变得异常简单且规范。 本章将详细介绍: 单元测试:如何编写基础测试用例。 表格驱动测试:Go 社区推荐的最佳实践,如何用更少的代码覆盖更多的场景。 基准测试 (Benchmark):如何科学地测量代码的性能。 1. 基础单元测试 Go 的测试文件必须以 _test.go 结尾,测试函数必须以 Test 开头。 假设我们要测试一个简单的加法函数(保存为 math.go): 1package math 2 3func Add(a, b int) int { 4 return a + b 5} 对应的测试文件(保存为 math_test.go): 1package math 2 3import ( 4 "testing" 5) 6 7// 测试函数签名:func TestXxx(t *testing.T) 8func TestAdd(t *testing.T) { 9 got := Add(1, 2) 10 want := 3 11 12 if got != want { 13 // t.Errorf 输出错误信息,测试继续执行 14 // t.Fatalf 输出错误信息,并立即终止当前测试函数 15 t.Errorf("Add(1, 2) = %d; want %d", got, want) 16 } 17} 运行命令: ...

2025-02-19 · 2 min · 356 words · 老墨

GoLang教程——标准库精选

Go 被誉为“自带电池”(Batteries Included)的语言,因为其标准库极其丰富,涵盖了开发中 80% 的需求。本章精选了几个最常用、最基础的标准库,展示它们的常规用法。 示例代码 1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "time" 8) 9 10type User struct { 11 ID int `json:"id"` 12 Name string `json:"name"` 13 // json:"-" 表示在 JSON 中忽略该字段 14 Email string `json:"-"` 15} 16 17func main() { 18 // 1. fmt - 格式化 19 age := 18 20 // Sprintf 返回字符串,Printf 直接打印 21 msg := fmt.Sprintf("I am %d years old", age) 22 fmt.Println(msg) 23 24 // 2. strings - 字符串处理 25 str := " hello world " 26 fmt.Println(strings.TrimSpace(str)) // 去除首尾空格 27 fmt.Println(strings.Contains(str, "world")) // true 28 fmt.Println(strings.Join([]string{"a", "b"}, "-")) // a-b 29 30 // 3. time - 时间处理 31 now := time.Now() 32 // Go 的独特日期格式化模板:2006-01-02 15:04:05 33 fmt.Println(now.Format("2006-01-02 15:04:05")) 34 35 // 时间计算 36 later := now.Add(time.Hour) 37 fmt.Println(later.Sub(now)) // 1h0m0s 38 39 // 4. encoding/json - JSON 编解码 40 user := User{ID: 1, Name: "Hank", Email: "admin@hankmo.com"} 41 42 // 序列化 Struct -> JSON 43 jsonData, _ := json.Marshal(user) 44 fmt.Println(string(jsonData)) // {"id":1,"name":"Hank"} 45 46 // 反序列化 JSON -> Struct 47 var u2 User 48 json.Unmarshal(jsonData, &u2) 49 fmt.Println(u2.Name) 50} 关键点解释 fmt 包 Print / Println:标准输出。 Printf:格式化输出,常用占位符 %s(字符串), %d(整数), %v(自动推导), %+v(显示结构体字段名)。 time 包 格式化陷阱:必须记住基准时间 2006-01-02 15:04:05(助记:1月2号3点4分5秒06年)。 time.Duration:表示时间段,单位是纳秒。常用 10 * time.Second。 strings 包 提供了高效的不可变字符串操作:Split, Replace, Count, HasPrefix 等。 频繁拼接字符串推荐使用 strings.Builder。 encoding/json 包 利用 Struct Tags(如 `json:"name"`)来控制序列化行为。 只会序列化结构体中的 导出字段(首字母大写)。 小结 熟练使用标准库可以避免重复造轮子。除了上面介绍的,还有 net/http(Web服务)、io/ioutil(文件读写)、os(系统操作)等也非常重要。 ...

2025-01-12 · 2 min · 238 words · 老墨

GoLang教程——并发进阶

在掌握了 Goroutine 和 Channel 的基础知识后,我们需要了解 Go 标准库 sync 包提供的各种同步工具。这些工具能帮助我们更好地控制并发程序的执行流程,避免数据竞争,提高程序的可靠性。 sync.WaitGroup:等待一组 Goroutine 完成 基本用法 WaitGroup 用于等待一组 Goroutine 完成。它有三个方法: Add(delta int):增加计数器 Done():减少计数器(相当于 Add(-1)) Wait():阻塞直到计数器为 0 1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9func worker(id int, wg *sync.WaitGroup) { 10 defer wg.Done() // 函数结束时调用 Done() 11 12 fmt.Printf("Worker %d starting\n", id) 13 time.Sleep(time.Second) 14 fmt.Printf("Worker %d done\n", id) 15} 16 17func main() { 18 var wg sync.WaitGroup 19 20 // 启动 5 个 worker 21 for i := 1; i <= 5; i++ { 22 wg.Add(1) // 每启动一个 goroutine,计数器 +1 23 go worker(i, &wg) 24 } 25 26 wg.Wait() // 等待所有 worker 完成 27 fmt.Println("All workers completed") 28} 注意事项 Add() 必须在 Wait() 之前调用 Add() 通常在启动 goroutine 之前调用,而不是在 goroutine 内部 必须传递 WaitGroup 的指针,而不是值拷贝 sync.Mutex:互斥锁 基本用法 Mutex(互斥锁)用于保护共享资源,确保同一时间只有一个 goroutine 可以访问。 ...

2024-12-05 · 5 min · 898 words · 老墨

GoLang教程——并发基础

Go 语言之所以在云原生时代大放异彩,很大程度上归功于其原生支持的高效并发模型。Go 采用了 Goroutine(协程)和 Channel(通道)来实现并发,这种模式被称为 CSP(通信顺序进程)。本章将带你入门 Go 的并发世界。 示例代码 1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8// 一个模拟耗时任务的函数 9func worker(id int, jobs <-chan int, results chan<- int) { 10 for j := range jobs { 11 fmt.Printf("Worker %d started job %d\n", id, j) 12 time.Sleep(time.Second) // 模拟耗时 1 秒 13 fmt.Printf("Worker %d finished job %d\n", id, j) 14 results <- j * 2 // 将结果发送回 results 通道 15 } 16} 17 18func main() { 19 // 创建两个通道:任务通道和结果通道 20 // 设置缓冲区大小为 10,防止阻塞 21 jobs := make(chan int, 10) 22 results := make(chan int, 10) 23 24 // 启动 3 个 Goroutine (Worker) 25 // 它们会并发地从 jobs 通道抢任务做 26 for w := 1; w <= 3; w++ { 27 go worker(w, jobs, results) 28 } 29 30 // 发送 5 个任务 31 for j := 1; j <= 5; j++ { 32 jobs <- j 33 } 34 close(jobs) // 关闭任务通道,告知 Worker 没有新任务了 35 36 // 接收并打印结果 37 for a := 1; a <= 5; a++ { 38 <-results 39 } 40 41 fmt.Println("All jobs finished!") 42} 关键点解释 Goroutine (协程) 使用 go 关键字 即可启动一个新的协程。例如 go funcName()。 协程比线程更轻量,启动成本极低,Go 运行时可以在少数几个 OS 线程上调度成千上万个 Goroutine。 main 函数本身也是一个 Goroutine。当 main 结束时,所有其他 Goroutine 都会被强制终止。 Channel (通道) 通道是 Goroutine 之间通信的管道。遵循“不要通过共享内存来通信,而要通过通信来共享内存”的原则。 定义:make(chan Type, capacity)。 发送:ch <- value。 接收:value := <-ch。 关闭:close(ch)。关闭后不能再发送,但可以继续接收已有的数据。 Range:for x := range ch 可以不断从通道读取数据,直到通道被关闭。 Select 选择器 select 语句是 Go 并发编程中的重要工具,用于同时等待多个通道操作。它的语法类似于 switch,但专门用于通道。 ...

2024-11-19 · 3 min · 428 words · 老墨

GoLang教程——错误处理

Go 对错误处理的态度非常直白:错误就是一种值(Values),不是异常(Exceptions)。我们通过函数返回值来传递错误,并显式地检查它们。只有在真正的不可恢复情况(如数组越界)下,才会使用 panic。 示例代码 1package main 2 3import ( 4 "errors" 5 "fmt" 6) 7 8// 定义一个除法函数,返回 result 和 error 9func divide(a, b int) (int, error) { 10 if b == 0 { 11 // 使用 errors.New 创建一个简单的错误对象 12 return 0, errors.New("cannot divide by zero") 13 } 14 return a / b, nil 15} 16 17// 演示 panic 和 recover 18func safeCall() { 19 // defer 必须在 panic 发生前定义 20 defer func() { 21 // recover() 捕获 panic,如果返回值不为 nil,说明发生了 panic 22 if r := recover(); r != nil { 23 fmt.Println("Recovered from panic:", r) 24 } 25 }() 26 27 panic("Something went wrong terribly!") 28 fmt.Println("This line will not execute") 29} 30 31func main() { 32 // 1. 标准错误处理 33 res, err := divide(10, 0) 34 if err != nil { 35 fmt.Println("Error:", err) 36 } else { 37 fmt.Println("Result:", res) 38 } 39 40 // 2. 演示从 panic 中恢复 41 fmt.Println("Starting safeCall...") 42 safeCall() 43 fmt.Println("Program continues...") 44} 关键点解释 error 接口 Go 内置的 error 是一个接口,只包含一个方法 Error() string。 ...

2024-10-29 · 2 min · 217 words · 老墨

GoLang教程——结构体与接口

Go 语言没有传统的“类”和“继承”概念,而是通过 结构体 (Struct) 来封装数据,通过 接口 (Interface) 来定义行为。这种组合式设计(Composition over Inheritance)让代码更加灵活和解耦。 示例代码 1package main 2 3import "fmt" 4 5// 定义一个接口:只要实现了 Speak 方法的类型,都满足这个接口 6type Speaker interface { 7 Speak() string 8} 9 10// 定义结构体 Dog 11type Dog struct { 12 Name string 13} 14 15// Dog 实现 Speaker 接口 16// 注意:Go 中没有 implements 关键字,这是隐式实现的 17func (d Dog) Speak() string { 18 return "Woof!" 19} 20 21// 定义结构体 Cat 22type Cat struct { 23 Name string 24} 25 26// Cat 实现 Speaker 接口 27func (c Cat) Speak() string { 28 return "Meow!" 29} 30 31// 多态演示:接收任何 Speaker 32func introduce(s Speaker) { 33 fmt.Println(s.Speak()) 34} 35 36func main() { 37 d := Dog{Name: "Buddy"} 38 c := Cat{Name: "Kitty"} 39 40 introduce(d) // Woof! 41 introduce(c) // Meow! 42 43 // 接口类型断言 44 var s Speaker = d 45 if dog, ok := s.(Dog); ok { 46 fmt.Printf("It's a dog named %s\n", dog.Name) 47 } 48} 关键点解释 结构体 (Struct) 用户自定义的数据类型,是一组字段的集合。 匿名嵌入:可以在结构体中嵌入另一个结构体(不给字段名),从而直接访问其字段和方法,实现类似“继承”的效果。 1type Animal struct { Age int } 2type Dog struct { 3 Animal // 嵌入 4 Name string 5} 6// d := Dog{} 7// d.Age = 10 // 直接访问 接口 (Interface) 鸭子类型 (Duck Typing):如果一只鸟走起来像鸭子,叫起来像鸭子,那它就是鸭子。在 Go 中,只要一个类型实现了接口要求的所有方法,它就自动实现了该接口。 空接口 interface{}:不包含任何方法,因此任何类型都实现了空接口。类似于 Java 的 Object,常用于泛型容器。 类型断言 value, ok := interfaceVar.(Type):检查接口变量是否保存了特定类型的值。建议总是检查 ok 以避免 panic。 小结 结构体用于组织数据,接口用于抽象行为。掌握 Go 的隐式接口实现和组合模式,是编写地道 Go 代码的关键。 ...

2024-09-15 · 2 min · 227 words · 老墨

GoLang教程——数组、切片与映射

在处理数据集合时,Go 提供了三种主要方式:数组(Array)、切片(Slice)和映射(Map)。其中,切片和映射是日常开发中使用频率最高的数据结构。本章将介绍它们的区别与用法。 示例代码 1package main 2 3import "fmt" 4 5func main() { 6 // 1. 数组 (Array) 7 // 长度是类型的一部分,固定不可变 8 var arr [3]int = [3]int{10, 20, 30} 9 fmt.Println("Array:", arr) 10 11 // 2. 切片 (Slice) 12 // 动态数组,引用类型 13 slice := []int{1, 2, 3, 4, 5} 14 15 // 操作:切取子集 [start:end] (左闭右开) 16 subSlice := slice[1:3] // 包含索引 1, 2 的元素 -> {2, 3} 17 fmt.Println("SubSlice:", subSlice) 18 19 // 操作:追加元素 20 // 当容量不足时,append 会自动扩容 21 slice = append(slice, 6) 22 fmt.Println("Appended Slice:", slice) 23 24 // 3. 映射 (Map) 25 // 键值对集合,类似 Python 的 dict 或 Java 的 HashMap 26 scores := make(map[string]int) 27 scores["Alice"] = 95 28 scores["Bob"] = 88 29 30 // 检查键是否存在 31 // val 是值,ok 是布尔值(存在为 true) 32 if score, ok := scores["Alice"]; ok { 33 fmt.Printf("Alice's score is %d\n", score) 34 } 35 36 // 删除键值对 37 delete(scores, "Bob") 38 39 // 遍历 Map (注意:遍历顺序是随机的) 40 for name, score := range scores { 41 fmt.Printf("%s: %d\n", name, score) 42 } 43} 关键点解释 数组 (Array) 声明:[Length]Type。例如 [5]int 和 [10]int 是完全不同的类型。 数组是 值类型,赋值或传递给函数时会发生拷贝(复制整个数组)。一般很少直接使用。 切片 (Slice) 声明:[]Type(不指定长度)。 切片本质上是对底层数组的一个“视窗”,包含三个属性:指针、长度 (len)、容量 (cap)。 推荐:使用 make([]Type, len, cap) 创建切片,或使用字面量 []Type{...}。 append 函数可能会返回一个新的切片引用(当发生扩容时),所以必须重新赋值:s = append(s, val)。 映射 (Map) 声明:map[KeyType]ValueType。 必须使用 make 初始化,或者使用字面量。未初始化的 map 是 nil,向其写入会导致 panic。 Key 必须是支持比较(==)的类型(如 int, string),切片不能作为 Key。 所有的 Map 操作都不是线程安全的(并发读写需要加锁)。 小结 数组长度固定,切片长度动态。优先使用切片。 Map 处理键值对,查找速度快。 切片和 Map 都是引用类型,传递给函数时不会拷贝底层数据,效率高。 练习题 创建一个包含 10 个元素的整型切片,使用 range 遍历并打印所有偶数。 统计一段英文文本中每个字符出现的次数,使用 map[rune]int 存储并打印结果。

2024-08-22 · 2 min · 230 words · 老墨

GoLang教程——控制结构

Go 语言的控制结构非常精简。它没有 while 或 do-while,只有一个强大的 for 循环。同时,switch 更加智能,if 支持初始化语句。本章还将介绍 Go 独有的资源管理神器 —— defer。 示例代码 1package main 2 3import "fmt" 4 5func main() { 6 // 1. if 语句 7 x := 10 8 // 条件判断不需要小括号 () 9 if x > 5 { 10 fmt.Println("x is large") 11 } 12 13 // if 支持初始化语句:先执行初始化,再判断 14 if y := x * 2; y > 15 { 15 fmt.Println("y is", y) // y 的作用域仅限于 if 块 16 } 17 18 // 2. switch 语句 19 day := "Mon" 20 switch day { 21 case "Mon": 22 fmt.Println("Start of week") 23 // Go 默认不需要 break,自动终止 24 case "Fri": 25 fmt.Println("Weekend is coming") 26 default: 27 fmt.Println("Other day") 28 } 29 30 // 3. for 循环:Go 唯一的循环结构 31 // 形式一:类似于 C/Java 的 for 32 for i := 0; i < 3; i++ { 33 fmt.Print(i, " ") 34 } 35 fmt.Println() 36 37 // 形式二:类似于 while 38 count := 3 39 for count > 0 { 40 fmt.Print(count, " ") 41 count-- 42 } 43 fmt.Println() 44 45 // 4. defer 延迟执行 46 // 常用于资源释放,函数返回前才会执行 47 defer fmt.Println("Exiting main function...") 48 fmt.Println("Doing some work...") 49} 关键点解释 If 条件 条件表达式 不需要 小括号 ()。 大括号 {} 是 必须 的,且左大括号不能换行。 支持 if statement; condition 写法,常用于处理错误:if err := doSomething(); err != nil { ... }。 Switch 分支 默认不需要 break,匹配到一个 case 后自动停止。 如果想继续执行下一个 case,需要显式使用 fallthrough(极少用)。 switch 后可以没有表达式,直接在 case 中写条件判断,替代复杂的 if-else 链。 For 循环 Go 只有 for。 ...

2024-08-01 · 2 min · 286 words · 老墨

GoLang教程——函数与方法

函数是 Go 程序的基本构建单元。Go 语言中的函数非常灵活,支持多返回值,并且可以为特定的类型定义方法(类似于面向对象中的成员函数)。本章将带你快速掌握函数的定义、调用以及方法的绑定。 示例代码 先看一个完整的可运行示例,展示了普通函数、多返回值以及方法的用法。 1package main 2 3import "fmt" 4 5// add 是一个普通函数,接收两个 int 参数,返回一个 int 结果 6func add(a, b int) int { 7 return a + b 8} 9 10// swap 演示多返回值,交换输入的两个字符串 11func swap(x, y string) (string, string) { 12 return y, x 13} 14 15// 定义一个简单的结构体 User 16type User struct { 17 Name string 18 Age int 19} 20 21// SayHello 是绑定到 User 结构体的方法 22// (u User) 称为接收者(Receiver) 23func (u User) SayHello() { 24 fmt.Printf("Hello, my name is %s and I am %d years old.\n", u.Name, u.Age) 25} 26 27// 只有指针接收者才能修改结构体内部的值 28func (u *User) Grow() { 29 u.Age++ 30} 31 32func main() { 33 // 1. 调用普通函数 34 sum := add(10, 20) 35 fmt.Println("10 + 20 =", sum) 36 37 // 2. 调用多返回值函数 38 a, b := swap("hello", "world") 39 fmt.Println(a, b) // world hello 40 41 // 3. 调用方法 42 user := User{Name: "Hank", Age: 18} 43 user.SayHello() 44 45 // 调用指针接收者方法修改状态 46 user.Grow() 47 fmt.Printf("Age after grow: %d\n", user.Age) // 19 48} 关键点解释 函数声明 使用 func 关键字。类型在变量名之后,这是 Go 的特色之一。 ...

2024-07-17 · 2 min · 237 words · 老墨

Golang中检测 Data Race

Data Race概念 Data Race (数据竞争),是并发系统中最常见和最难调试的错误类型之一,当两个线程(协程)同时访问同一个变量并且至少其中一次访问是写入时,就会发生 Data Race 。Java 内存模型(JMM)定义了明确的 Happens-Before 规则和提供 Volatile 机制来初步保证程序的执行顺序,Go内存模型(GMM)同样也有相同的规则定义,此外 Golang 还提供了一种检测 Data Race 的机制,帮助开发者更好的分析并发代码的正确性。 go在1.1版本时支持data race 检测。 Data Race 和 Race Condition 的区别: 这两个概念非常容易混淆,stackoverflow上也有讨论这个问题,其实我认为这篇文章很好的区别了它们,所以我进行了翻译。 检测 Data Race 使用 -race 参数可以用来检测这种竞态: 1$ go test -race mypkg // to test the package 2$ go run -race mysrc.go // to run the source file 3$ go build -race mycmd // to build the command 4$ go install -race mypkg // to install the package 1func main() { 2 c := make(chan int) 3 m := make(map[string]int) 4 go func() { 5 m["a"] = 1 // 访问map冲突 6 c <- 1 7 }() 8 m["a"] = 2 // 访问map冲突 9 <-c 10 for k, v := range m { 11 fmt.Printf("key = %v, val = %v\n", k, v) 12 } 13} 示例程序中,两个 goroutine 同时读写 map 存在竞态,可能造成数据不正确的情况,但是错误难以发现,通过执行时添加 -race 选择可以检测: ...

2024-06-20 · 5 min · 980 words · 老墨

Data Race vs Race Condition

原文地址: https://blog.regehr.org/archives/490, 翻译并略作改动。 竞态条件(race Condition)是当事件的时间或顺序影响程序的正确性时发生的缺陷。一般来说,需要某种外部计时或排序非确定性来产生竞态条件;典型的例子有上下文切换、操作系统信号、多处理器上的内存操作和硬件 中断。 当程序中有两次内存访问时,就会发生数据竞争(Data Race): 目标为同一内存位置 由两个线程同时执行 不是读取操作 不是同步操作 上边这个定义来自微软研究院的 Sebastian Burckhardt。该定义的两个方面需要注意: “同时”意味着没有像锁这样的东西强制一个操作在另一个操作之前或之后发生。 “不是同步操作”是指程序可能包含特殊的内存操作,例如用于实现锁的操作,这些操作本身并不同步。 在实践中,它们两者存在相当大的重合:许多 Race Condition 是由 Data Race 引起的,并且许多 Data Race 导致 Race Condition。另一方面,两者也可以相互独立,可能产生没有 Data Race 的 Race Condition,也可能产生没有 Race Condition 的 Data Race。 让我们从一个在两个银行账户之间转移资金的简单函数开始: transfer1 (amount, account_from, account_to) { if (account_from.balance < amount) return NOPE; account_to.balance += amount; account_from.balance -= amount; return YEP; } 当然,这并不是银行真正转移资金的方式,但这个例子非常有用。我们知道,账户余额应该是非负的,并且转移之后不能凭空创造(多出)或损失(丢失)金钱。当在没有外部同步的情况下从多个线程调用时,该函数会产生 Data Race(多个线程可以同时尝试更新帐户余额)和 Race Condition(在并行上下文中它将创造或损失金钱)。 我们可以尝试这样修复它: transfer2 (amount, account_from, account_to) { atomic { bal = account_from.balance; } if (bal < amount) return NOPE; atomic { account_to.balance += amount; } atomic { account_from.balance -= amount; } return YEP; } 这里的“atomic”(原子性)是由语言运行时实现的,也许简单地通过在原子块开始时获取线程互斥体(Mutex)并在结束时释放它,也许使用某种事务(Transaction),或者也许通过禁用中断 —— 出于示例的目的,只要 atomic 块内的代码以原子方式执行就能解决竞争问题。 ...

2024-06-18 · 2 min · 235 words · 老墨

Golang中singleflight的实现原理

我们知道了singleflight的用法,使用 singleflight 我们可以抑制多个请求,极大地节约带宽、增加系统吞吐量、提升性能。那么,singleflight 底层是如何实现的呢?本文我们来分析一番。 整体结构 singleflight 的核心是将同时段的多个请求抑制,只有一个请求能够真正请求资源,其他请求都阻塞。上一篇提到,singleflight的公开api仅包括: Group 对象: 它表示处理"相同数据"的一系列工作,在这里“重复请求”将会被抑制 Result 对象: 表示执行真正业务逻辑的结果对象 Do 方法: 执行请求抑制 DoChan 方法: 与Do相同,只是结果返回 <-chan Result 从这些api我们大致可以知道,调用 Do 或者 DoChan 方法就是我们所述的核心“请求”,可以猜测: 对于同一个 key,首先调用的会执行真正的逻辑,方法返回之前的后续所有相同的 key 调用都会阻塞,当第一个请求返回后,阻塞的这些调用就直接使用其返回值作为自己的返回值了。 顺着上述猜想的逻辑,我们看看singleflight的源码实现。代码不多,算上注释一共就200来行,我们来一一分析。 Group struct 首先看看 Group 的代码: 1type Group struct { 2 mu sync.Mutex // protects m 3 m map[string]*call // lazily initialized 4} Group 表示处理相同数据的一系列工作,这些工作存储到一个 map[string]*call 的结构中,为了保证并发安全,Group 内部持有 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认为是相同的请求。这个key其实就是底层map的key,下边会详细介绍 fn: 真正执行业务逻辑的方法,该方法没有任何参数且返回一个任意对象 interface{} 和一个 error,这就是真正执行业务逻辑的标准方法,我们把 fn 方法称为业务方法 返回值: ...

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

使用 Let‘S Encrypt 为您的网站申请免费证书

网站的https证书过期了,一直使用阿里云的免费ssl证书,但是现在阿里云调整了策略,证书有效期从1年缩短到3个月了,所以我决定放弃阿里云转而使用 Let’s Encrypt 申请免费证书。 简介 Let’s Encrypt 是一家免费、开放、自动化的公益性证书颁发机构(CA), 由互联网安全研究组(ISRG)运作,详细介绍可以看这里。 申请证书 按照官方文档,对于没有命令行访问权限的网站,比如wordpress、cPanel等,可以通过控制台设置或者申请后手动上传证书;而我的网站服务器托管在阿里云,拥有命令行访问权限,所以直接使用官方推荐的 Certbot ACME 客户端来管理证书,这里详细记录下步骤。 CertBot 文档有详细的安装步骤,我的 ubuntu14 上的步骤如下: 安装 snapd snap 是linux的应用程序包,可以从其自身的snap store中安装、管理软件,有点类似包管理器如 apt、yum,但是不依赖linux发行版,安全、跨平台且无依赖;而 snapd 则是一个自动管理和维护 snap 的后台服务,它们之间的区别请参阅官方文档。 Ubuntu 16.04LTS版及之上的版本已经集成了snapd,无需再安装了,而我的是 14,所以需要手动安装。 sudo apt update sudo apt install snapd 测试是否安装成功,可以安装官方的 hello-world 程序: $ snap install hello-world 2024-06-09T16:15:39+08:00 INFO Waiting for automatic snapd restart... hello-world 6.4 from Canonical✓ installed 执行并成功输出信息表示安装成功: $ hello-world Hello World! 删除 certbot-auto 和任何 Certbot OS 软件包 如果您使用操作系统包管理器(如 apt 、 dnf 或 yum 安装了任何 Certbot 包,则应在安装 Certbot snap 之前删除它们,以确保运行命令 certbot 时,将使用 snap,而不是从操作系统包管理器进行安装。执行此操作的确切命令取决于您的操作系统,但常见的示例是 sudo apt-get remove certbot 、 sudo dnf remove certbot 或 sudo yum remove certbot 。 ...

2024-06-09 · 2 min · 374 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 · 老墨

Rust 学习笔记 03:函数

Rust 学习笔记 03:函数 “Talk is cheap. Show me the code.” – Linus Torvalds 在这一课,我们来聊聊编程语言中最基础的构建块——函数。 如果你熟悉 Go,你可能会觉得:“函数嘛,不就是 func 换成了 fn,大括号里写逻辑吗?” 确实,表面上看区别不大。但 Rust 引入了一个对 Go 开发者来说比较新颖的概念:语句 (Statements) 与 表达式 (Expressions) 的严格区分。这直接改变了代码的书写习惯(和逼格)。 1. 定义函数 Rust 使用 snake_case (蛇形命名法) 来命名函数,所有字母小写,单词间用下划线分开。 1fn main() { 2 println!("Hello, world!"); 3 another_function(); 4} 5 6fn another_function() { 7 println!("Another function."); 8} Go 也是这么写的(除了大括号位置 Go 强制不换行,Rust 推荐不换行但没强制)。 2. 参数与类型 和 Go 一样,Rust 是强类型语言,函数参数必须声明类型。这点没得商量。 1fn print_labeled_measurement(value: i32, unit_label: &str) { 2 println!("The measurement is: {}{}", value, unit_label); 3} 3. 语句 vs 表达式 (The Twist) 这是本节的重点! ...

2024-05-18 · 2 min · 275 words · 老墨

Python教程12:列表进阶与推导式高级

Python教程12:列表进阶与推导式高级 “熟能生巧,巧能生精。” 在第8课我们学习了列表推导式的基础,今天我们深入探讨列表推导式的高级技巧和列表的进阶操作,让你的代码更加Pythonic和高效。 1. 回顾:列表推导式基础 1# 基础语法 2squares = [x**2 for x in range(10)] 3 4# 带条件 5evens = [x for x in range(10) if x % 2 == 0] 6 7# if-else 8result = [x if x > 0 else 0 for x in [-1, 2, -3, 4]] 2. 嵌套列表推导式 二维列表展平 1# 传统方法 2matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 3flat = [] 4for row in matrix: 5 for num in row: 6 flat.append(num) 7 8# 列表推导式 9flat = [num for row in matrix for num in row] 10print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9] 理解技巧:从左到右阅读,就像嵌套的for循环。 创建二维列表 1# 创建3×3矩阵 2matrix = [[0 for _ in range(3)] for _ in range(3)] 3print(matrix) 4# [[0, 0, 0], [0, 0, 0], [0, 0, 0]] 5 6# 创建乘法表 7table = [[i*j for j in range(1, 10)] for i in range(1, 10)] 8 9# 注意:不要这样创建二维列表 10# bad = [[0] * 3] * 3 # 错误!所有行是同一个对象 多重嵌套with条件 1# 找出两个列表的所有组合(有条件) 2a = [1, 2, 3] 3b = [3, 4, 5] 4 5# 找出和大于5的组合 6result = [(x, y) for x in a for y in b if x + y > 5] 7print(result) # [(2, 4), (2, 5), (3, 3), (3, 4), (3, 5)] 3. 列表推导式vs传统循环 1# 性能对比示例 2import time 3 4# 方法1:传统for循环 5start = time.time() 6result1 = [] 7for i in range(100000): 8 result1.append(i**2) 9time1 = time.time() - start 10 11# 方法2:列表推导式 12start = time.time() 13result2 = [i**2 for i in range(100000)] 14time2 = time.time() - start 15 16print(f"传统循环:{time1:.4f}秒") 17print(f"列表推导式:{time2:.4f}秒") 18# 列表推导式通常快20-30% 4. 字典和集合推导式进阶 字典推导式高级用法 1# 统计字符出现次数 2text = "hello world" 3char_count = {char: text.count(char) for char in set(text) if char != ' '} 4 5# 从列表创建索引字典 6fruits = ['apple', 'banana', 'cherry'] 7fruit_index = {fruit: i for i, fruit in enumerate(fruits)} 8 9# 嵌套字典推导式 10students = ['Alice', 'Bob'] 11subjects = ['Math', 'English'] 12grades = { 13 student: {subject: 0 for subject in subjects} 14 for student in students 15} 集合推导式妙用 1# 去重并转换 2numbers = [1, -2, 3, -4, 5] 3abs_unique = {abs(n) for n in numbers} # {1, 2, 3, 4, 5} 4 5# 找差异 6list1 = [1, 2, 3, 4, 5] 7list2 = [4, 5, 6, 7, 8] 8diff = {x for x in list1 if x not in list2} # {1, 2, 3} 5. 生成器表达式深入 1# 列表推导式:立即生成,占内存 2squares_list = [x**2 for x in range(1000000)] 3 4# 生成器表达式:按需生成,省内存 5squares_gen = (x**2 for x in range(1000000)) 6 7# 使用生成器 8total = sum(x**2 for x in range(1000000)) 9 10# 生成器只能遍历一次 11gen = (x for x in range(5)) 12print(list(gen)) # [0, 1, 2, 3, 4] 13print(list(gen)) # [](已耗尽) 6. 列表的高级操作 zip和enumerate进阶 1# zip并行遍历 2names = ['Alice', 'Bob', 'Charlie'] 3ages = [25, 30, 35] 4cities = ['Beijing', 'Shanghai', 'Guangzhou'] 5 6# 创建字典 7people = [ 8 {'name': n, 'age': a, 'city': c} 9 for n, a, c in zip(names, ages, cities) 10] 11 12# enumerate with start 13for i, name in enumerate(names, start=1): 14 print(f"{i}. {name}") filter和map结合推导式 1# 虽然有filter和map,但推导式更清晰 2numbers = range(1, 11) 3 4# filter + map方式 5result1 = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, numbers))) 6 7# 推导式方式(更清晰) 8result2 = [x**2 for x in numbers if x % 2 == 0] 7. 实战案例 案例1:矩阵转置 1matrix = [ 2 [1, 2, 3], 3 [4, 5, 6], 4 [7, 8, 9] 5] 6 7# 转置 8transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))] 9print(transposed) 10# [[1, 4, 7], [2, 5, 8], [3, 6, 9]] 11 12# 或使用zip 13transposed = [list(col) for col in zip(*matrix)] 案例2:笛卡尔积 1colors = ['红', '黑'] 2sizes = ['S', 'M', 'L'] 3products = [f"{color}-{size}" for color in colors for size in sizes] 4# ['红-S', '红-M', '红-L', '黑-S', '黑-M', '黑-L'] 案例3:数据清洗 1# 清洗CSV数据 2raw_data = [ 3 " Alice, 25 ", 4 "Bob,30", 5 " Charlie, 35 " 6] 7 8cleaned = [ 9 [item.strip() for item in row.split(',')] 10 for row in raw_data 11] 8. 何时不用推导式 虽然推导式简洁,但有时不适合: ...

2024-04-28 · 4 min · 744 words · 老墨

自由与梦想

今天终于读完了《我在北京送快递》一书,这本15万字的书前前后后读完居然花了两个多月的时间——习惯使然,我读什么书都喜欢字斟句酌,这一点让我有些苦恼:一方面是因为进度非常缓慢,想象一下,读一本人物传记我还在拿着笔一边阅读一边给不认识的字词标注笔记……,另一方面,由于阅读的周期很长,导致容易半途而废,可能前半部分阅读的很仔细,后半部分就走马观花甚至放弃了。不过这本书我还算比较认真的读完了,我想谈一谈我对“自由”的理解。 作者在尾声的“后记”一章中阐述了自己对“自由”的观点:这些年来自己经常处于一边工作、一边写作的生活状态中,工作时无法全心投入写作,而写作时也无法积极投身工作,作者自己希望的自由是“在高度发展上的个人意识的个人追求和自我实现”。他认为,追求自己热衷的事情,形成与别人不同的自我意识上的区别,那么这种追求是自由的;相反,表面上看似自由,却并没有实现自我价值,只是迫于某种原因而不得不去做,又或者是安于现状,非个人追求或无法实现自我,那么这些都不能算是真正的自由。 作者写到,在迫于生计而不得不付诸大量时间和精力在工作上时,自己便无法专注于自己喜欢的事情——写作,自身也被疲倦、困意、复杂的人际关系所折磨,,身心俱疲,就算挣到了钱但自己并不快乐,这种生活不能算是自由;而之后回到老家,全身心投入写作,不必为生计而担忧,每天作者自己喜欢的事情——阅读、写作、与其他作者交流写作创意等等,自己的阅读量、写作水平都有很大的提升,这在知识的高度上与其他人拉开了差距、形成了区别,自身的追求有了一定的成果,自己也觉得开心、快乐,这样的生活是自由的。 我很喜欢作者的这个观点。简而言之,作者所述的自由,在我看来是“做自己想做的事,走自己想走的路”。 每个人心中都会有梦想,那就是我们最想做的事情,最想走的路,只是多数时候我们为生活所迫东奔西走,仿佛随波逐流的沙粒,被生活磨平了棱角,早就忘记了自己的梦想是什么了。**“人们只有两条路,一条需要用心走,叫做梦想;另一条需要用脚走,叫做现实。”**诚然,现实与梦想仿佛总是相对立的两面。对于大多数人而言,梦想往往是高不可攀的,只能深埋于心,为了生计不得不放弃梦想而从事自己并不喜欢的工作的人不计其数,为追逐梦想而奋斗此生最终获得成功的人寥寥无几。 在人生的道路上,现实与梦想常常交织着,而在这交织之中,自由似乎成了一种奢侈。对许多人而言,梦想是一种遥不可及的幻影,而现实却是残酷的枷锁,将他们紧紧束缚。在这样的环境下,自由似乎只存在于幻想之中。 普通的农民每天的工作就是完成农务,整日与农田、二十四节气打交道,农忙时努力播种、收获,农闲时与朋友们一起打打牌、聊聊天。我相信,他们大多觉得生活本就如此,自给自足也挺好,除非生活实在拮据迫不得已才会跑出去打工赚钱。此时,或许他们早已习惯了这样的生活,早已忘记年轻时候自己想要做的事情了。对于他们而言,无论忙碌或是清闲,可能都不重要,年复一年重复着同样的事情,循规蹈矩地过着自己的生活,这就是最简单的自由和幸福吧。 在车水马龙的城里,白领们的知识水平比普通农民要高出许多,他们终日奔波忙碌,所选择的工作就是自己所追求的梦想吗?我相信大多连“事业”都称不上。他们所做的只是“为老板打工”,赚取自己的劳动报酬罢了,他们的工作心态总是“尽力完成我能力范围内的工作”的得过且过,很少有人会将自己的工作真正的当成一份事业来打拼,因为就算你再努力,那也是为别人赚钱,对自己并没有多大意义。所以,大多数白领会默默地选择平庸,只是在偶尔看到少数人成功之后才能激起心中的激情,也许那只是害怕自己被周围人超越的恐慌,然后疯狂地努力一段时间,随后逐渐发现无论自己多努力似乎都无法提升自己到一个新的高度,慢慢难以坚持最后果断放弃并又再次平庸下去,这就样反反复复,最终一无所成。 当我们回首自己的生活时,是否还记得小时候那个满怀梦想的自己?那个对未来充满信心、对世界充满好奇的自己是否还在内心深处?然而,随着年岁的增长,生活的压力和社会的现实往往会让我们逐渐放弃那些美好的梦想。我们被迫在现实的洪流中奔波,为了生计为了生活,不得不选择一条看似安稳但缺少激情的道路。 梦想的实现谈何容易?马云当初创业时,初创团队“十八罗汉”吃住都其家里,十几人就挤在几十平方的房子里,他们没日没夜的工作,饿了就吃泡面,困了就打地铺小睡一会儿……但是,团队人人都充满着激情,没有一个人轻言放弃,因为他们知道他们现在是在为了梦想而奋斗,奋斗着并奋不顾身。就在阿里巴巴上市的那一天,这十八罗汉人人都身价过亿、功成名就、梦想成真。回望这一路坎坷,我想他们应该是无怨无悔了吧?至少,为了梦想,再苦再累也都是甘之如饴吧。这样的生活,看似处处是桎梏,却总能令人无限神往,这又何尝不是一种自由呢?马云当然属于成功者,这些年义无反顾的追梦,等到如今大获成功之后,可能他才发现原来自己的人生还有其他更想做的事情,所以他开办学校、拍电影、搞演唱会……有钱真任性,但是他有任性的资本,试问此时,为所欲为的马云是否获得了真正的自由呢?答案只有他自己知道,看似风光无限,可能高处不胜寒的寂寞只有他自己懂得了。 当我们审视自己的生活,回顾梦想与现实的交织,或许我们会发现,自由并非遥不可及的幻影,而是一种内心的坚定和勇气。在生活的征途中,或许我们会因为生计而忙碌,会因为社会的压力而迷失,但只要我们保持对梦想的信念,不忘初心,奋力前行,那梦想的光芒就会一直照亮前行的路。或许成功并非是终点,而是一种过程,而真正的自由,其实就是坚持不懈地追寻内心的信仰,敢于实现自己的梦想,敢于选择一条不一样的道路。因为只有在追逐梦想的路上,我们才能找到真正属于自己的自由和幸福。愿我们都能保持对梦想的热爱,勇敢追寻内心的声音,活出真正的自己,活出自由的精彩人生。

2024-04-26 · 1 min · 11 words · 老墨

Python教程11:列表基础

Python教程11:列表基础 “工欲善其事,必先利其器。” 列表(List)是Python中最常用的数据结构之一,就像一个可以随意增删改查的购物清单。今天我们深入学习列表的基础操作,为后续学习打好基础。 1. 什么是列表 列表是一个有序的、可变的元素集合,可以存储不同类型的数据。 1# 创建列表 2empty_list = [] # 空列表 3numbers = [1, 2, 3, 4, 5] # 整数列表 4mixed = [1, "hello", 3.14, True] # 混合类型 5nested = [[1, 2], [3, 4], [5, 6]] # 嵌套列表 6 7# 使用list()函数 8from_string = list("Python") # ['P', 'y', 't', 'h', 'o', 'n'] 9from_range = list(range(5)) # [0, 1, 2, 3, 4] 2. 访问列表元素 索引访问 1fruits = ["苹果", "香蕉", "橙子", "葡萄", "西瓜"] 2 3# 正向索引(从0开始) 4print(fruits[0]) # 苹果 5print(fruits[1]) # 香蕉 6 7# 负向索引(从-1开始) 8print(fruits[-1]) # 西瓜(最后一个) 9print(fruits[-2]) # 葡萄(倒数第二个) 10 11# 索引越界会报错 12# print(fruits[10]) # IndexError 切片访问 1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 2 3# [start:stop:step] 4print(numbers[2:5]) # [2, 3, 4] 5print(numbers[:5]) # [0, 1, 2, 3, 4] 6print(numbers[5:]) # [5, 6, 7, 8, 9] 7print(numbers[::2]) # [0, 2, 4, 6, 8](步长为2) 8print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0](反转) 9 10# 负索引切片 11print(numbers[-3:]) # [7, 8, 9] 12print(numbers[:-3]) # [0, 1, 2, 3, 4, 5, 6] 3. 修改列表 列表是可变的,可以直接修改元素: ...

2024-04-15 · 4 min · 750 words · 老墨

Rust 学习笔记 02:变量与可变性

Rust 学习笔记 02:变量与可变性 “Stability is not immobility.” – Prince Metternich 习惯了 Go 语言的 var 和 :=,第一次写 Rust 时,最让人抓狂的报错一定是 cannot assign twice to immutable variable。 在 Go 里,变量生来就是可变的(除了 const),但在 Rust 里,变量生来就是不可变的。 这就像是:Go 是一个自由奔放的游乐场,你想去哪就去哪;而 Rust 是一个戒备森严的博物馆,除非你申请了"触摸许可"(mut),否则只能看,不能摸。 1. 默认不可变 (Immutability) 在 Rust 中,当你写下: 1let x = 5; 注意: Rust 必须在语句末尾加上 “;",与 Java 一样,但是 Go 却不需要,这需要 Go 开发者习惯很久。 这不仅是定义了一个变量,更是立下了一个誓言:“x 的值就是 5,永远不会变。” 如果你试图打破誓言: 1x = 6; // 编译报错! 编译器会无情地告诉你:cannot assign twice to immutable variable x。 ...

2024-04-12 · 2 min · 350 words · 老墨

这本区块链爱好者必读作品终于迎来第三版

学习区块链的书籍我推荐这两本,我在这里已经推荐过一次: Mastering Bitcoin Mastering Ethereum 比特币开创了一门新的技术——区块链,而以太坊则在比特币的理念基础上进行再创新和升华,二者是近十多年来区块链技术的代表。因此,学习区块链技术,绝对离不开它们。恰好,这两本书就完全阐述了比特币和以太坊的技术。 这两本书都由同一名作者编写,他是一名有十多年区块链研究经验的爱好者,两本书的传播度非常广,是学习、研究区块链技术的必读作品。 《Mastering Ethereum》翻译为《精通以太坊》,目前该书可以在 github 上免费阅读,地址在这里,翻译版在这里。 《Mastering Bitcoin》中文译作《精通比特币》,本书详细介绍了比特币底层的实现逻辑,目前版本是第二版。但是随着比特币2017、2021两次的重大升级,添加了隔离见证、Taproot、Schnorr签名等重大改进,不论是逻辑上还是技术上都有着很大的学习难度,第二版是在2017升级之前刊印的,现在未免显得有点过时了。如果你想学习比特币的底层知识,可以在这里阅读到本书的中文版。 让人兴奋的是,2023年《Mastering Bitcoin》迎来了它的第三个版本,它由原作者 Andreas Antonopoulos 和另一位区块链爱好者 David Harding 联合编著,在第二版的基础上补全了这几年比特币的多个升级技术。可惜的是,本书目前还没有中文版本,但你可以直接在 github 上阅读英文版,地址在这里。 另外,为了便于阅读,我也fork了本书,编译并搭建了在线阅读网站,打开即可直接阅读,可以选择单页阅读整书也可以分章节阅读。 区块链技术的学习是一个枯燥和费力的过程,但历史的车轮总是向前推进的,技术在进步,坚持下来者方能迎接蜕变。 相关阅读: Bitcoin’s Taproot Upgrade: What You Should Know https://github.com/ajtowns/taproot-review

2024-03-20 · 1 min · 34 words · 老墨

Python教程10:第一个实用程序

Python 教程 10:第一个实用程序 “纸上得来终觉浅,绝知此事要躬行。” 经过前面 9 课的学习,我们已经掌握了 Python 的基础知识。今天,让我们把这些知识串起来,开发一个真正实用的程序:批量文件重命名工具。 1. 项目需求 开发一个命令行工具,能够: 批量重命名文件:支持添加前缀、后缀、替换文本 过滤文件:支持按扩展名、文件名模式过滤 预览模式:先预览修改,确认后再执行 撤销功能:记录操作,支持撤销 这个工具很实用,能解决日常工作中的真实问题。 2. 项目结构 file_renamer/ ├── file_renamer.py # 主程序 ├── renamer.py # 核心重命名逻辑 ├── utils.py # 工具函数 └── history.json # 操作历史记录 3. 核心功能实现 3.1 列出目录中的文件 1# utils.py 2import os 3 4def list_files(directory, extension=None, pattern=None): 5 """ 6 列出目录中的文件 7 8 Args: 9 directory: 目标目录 10 extension: 文件扩展名过滤(如'.txt') 11 pattern: 文件名模式(简单的包含匹配) 12 13 Returns: 14 文件路径列表 15 """ 16 files = [] 17 18 for filename in os.listdir(directory): 19 filepath = os.path.join(directory, filename) 20 21 # 只处理文件,忽略目录 22 if not os.path.isfile(filepath): 23 continue 24 25 # 扩展名过滤 26 if extension and not filename.endswith(extension): 27 continue 28 29 # 文件名模式过滤 30 if pattern and pattern not in filename: 31 continue 32 33 files.append(filepath) 34 35 return files 3.2 重命名逻辑 1# renamer.py 2import os 3import re 4 5class FileRenamer: 6 """文件重命名器""" 7 8 def __init__(self, directory): 9 self.directory = directory 10 self.changes = [] # 记录修改 11 12 def add_prefix(self, files, prefix): 13 """添加前缀""" 14 for filepath in files: 15 dirname = os.path.dirname(filepath) 16 filename = os.path.basename(filepath) 17 new_name = prefix + filename 18 new_path = os.path.join(dirname, new_name) 19 self.changes.append((filepath, new_path)) 20 21 def add_suffix(self, files, suffix): 22 """添加后缀(在扩展名前)""" 23 for filepath in files: 24 dirname = os.path.dirname(filepath) 25 filename = os.path.basename(filepath) 26 name, ext = os.path.splitext(filename) 27 new_name = name + suffix + ext 28 new_path = os.path.join(dirname, new_name) 29 self.changes.append((filepath, new_path)) 30 31 def replace_text(self, files, old_text, new_text): 32 """替换文件名中的文本""" 33 for filepath in files: 34 dirname = os.path.dirname(filepath) 35 filename = os.path.basename(filepath) 36 new_name = filename.replace(old_text, new_text) 37 new_path = os.path.join(dirname, new_name) 38 if filepath != new_path: # 只记录有变化的 39 self.changes.append((filepath, new_path)) 40 41 def preview(self): 42 """预览修改""" 43 if not self.changes: 44 print("没有要修改的文件") 45 return 46 47 print(f"\n将要进行 {len(self.changes)} 项修改:") 48 print("-" * 60) 49 for i, (old, new) in enumerate(self.changes, 1): 50 old_name = os.path.basename(old) 51 new_name = os.path.basename(new) 52 print(f"{i}. {old_name} -> {new_name}") 53 print("-" * 60) 54 55 def execute(self): 56 """执行重命名""" 57 if not self.changes: 58 print("没有要执行的操作") 59 return 60 61 success_count = 0 62 for old_path, new_path in self.changes: 63 try: 64 os.rename(old_path, new_path) 65 success_count += 1 66 except Exception as e: 67 print(f"错误:{old_path} -> {e}") 68 69 print(f"\n成功重命名 {success_count}/{len(self.changes)} 个文件") 70 71 # 保存操作历史 72 self.save_history() 73 74 def save_history(self): 75 """保存操作历史(简化版)""" 76 import json 77 from datetime import datetime 78 79 history_file = "history.json" 80 81 # 读取现有历史 82 history = [] 83 if os.path.exists(history_file): 84 with open(history_file, 'r', encoding='utf-8') as f: 85 history = json.load(f) 86 87 # 添加新记录 88 history.append({ 89 'time': datetime.now().isoformat(), 90 'changes': [(old, new) for old, new in self.changes] 91 }) 92 93 # 保存 94 with open(history_file, 'w', encoding='utf-8') as f: 95 json.dump(history, f, indent=2, ensure_ascii=False) 3.3 主程序 1# file_renamer.py 2#!/usr/bin/env python3 3# -*- coding: utf-8 -*- 4 5""" 6文件批量重命名工具 7 8用法: 9 python file_renamer.py 10""" 11 12import os 13from renamer import FileRenamer 14from utils import list_files 15 16def main(): 17 print("=" * 60) 18 print("文件批量重命名工具") 19 print("=" * 60) 20 21 # 获取目录 22 directory = input("\n请输入目录路径(留空使用当前目录):").strip() 23 if not directory: 24 directory = "." 25 26 if not os.path.exists(directory): 27 print(f"错误:目录 '{directory}' 不存在") 28 return 29 30 # 获取文件过滤条件 31 extension = input("文件扩展名过滤(如.txt,留空跳过):").strip() 32 if not extension: 33 extension = None 34 35 # 列出文件 36 files = list_files(directory, extension) 37 38 if not files: 39 print("没有找到符合条件的文件") 40 return 41 42 print(f"\n找到 {len(files)} 个文件") 43 44 # 创建重命名器 45 renamer = FileRenamer(directory) 46 47 # 操作菜单 48 while True: 49 print("\n请选择操作:") 50 print("1. 添加前缀") 51 print("2. 添加后缀") 52 print("3. 替换文本") 53 print("4. 预览修改") 54 print("5. 执行重命名") 55 print("0. 退出") 56 57 choice = input("\n请输入选择:").strip() 58 59 if choice == "1": 60 prefix = input("请输入前缀:") 61 renamer.add_prefix(files, prefix) 62 print("✓ 已添加前缀规则") 63 64 elif choice == "2": 65 suffix = input("请输入后缀:") 66 renamer.add_suffix(files, suffix) 67 print("✓ 已添加后缀规则") 68 69 elif choice == "3": 70 old_text = input("请输入要替换的文本:") 71 new_text = input("请输入新文本:") 72 renamer.replace_text(files, old_text, new_text) 73 print("✓ 已添加替换规则") 74 75 elif choice == "4": 76 renamer.preview() 77 78 elif choice == "5": 79 renamer.preview() 80 confirm = input("\n确认执行?(y/N):").strip().lower() 81 if confirm == 'y': 82 renamer.execute() 83 break 84 else: 85 print("已取消") 86 87 elif choice == "0": 88 print("再见!") 89 break 90 91 else: 92 print("无效的选择") 93 94if __name__ == "__main__": 95 main() 4. 使用示例 场景 1:照片重命名 假设有一批照片: ...

2024-03-18 · 4 min · 787 words · 老墨

Rust 学习笔记 01:简介与环境搭建

Rust 学习笔记 01:简介与环境搭建 “A language that doesn’t affect the way you think about programming, is not worth knowing.” – Alan Perlis 作为一名写了几年 Go 的程序员,习惯了 GC 的安逸,也忍受了 if err != nil 的繁琐。一直听说 Rust 有多强,但每次看到那陡峭的学习曲线和满屏的生命周期引用,都默默劝退。 2024 年了,是时候走出舒适区,挑战一下这个"编译器教你做人"的语言了。 这系列笔记不是官方教程的复读机,而是从一个 Go/Java 开发者的视角,记录学习过程中的困惑、对比和感悟。 1. 为什么要折腾自己? 如果 Go 是一把瑞士军刀,简单实用;那 Rust 就像是一把手术刀,精准锋利,但由于太锋利,很容易割到手。 对于 Go 开发者来说,Rust 的吸引力在于: 零成本抽象:不用担心封装会带来性能损耗。 没有 GC:从此告别 STW (Stop The World),虽然 Go 的 GC 已经很快了,但有些场景下,完全控制内存是刚需。 安全性:编译器会在编译阶段就拦下绝大多数内存错误和并发数据竞争问题。 2. 环境搭建 Rust 的安装体验和 Go 差不多,甚至更现代。 macOS/Linux 一行命令搞定: ...

2024-03-05 · 1 min · 203 words · 老墨

Python教程09:Python编码规范(PEP 8)

Python 教程 09:Python 编码规范(PEP 8) “代码的阅读次数远远多于编写次数。” PEP 8 是 Python 官方的编码规范,定义了如何写出"Pythonic"的代码。遵循这些规范,你的代码会更专业、更易读、更容易被其他 Python 程序员理解。 1. 什么是 PEP 8 PEP (Python Enhancement Proposal) 是 Python 增强提案。PEP 8 专门定义了 Python 代码的风格指南。 核心思想: 代码更多是被阅读,而不是被编写 一致性很重要 可读性至上 完整文档:https://peps.python.org/pep-0008/ 2. 缩进和空格 使用 4 个空格缩进 1# 正确 2def hello(): 3 print("Hello") 4 if True: 5 print("World") 6 7# 错误:使用Tab或2个空格 8def hello(): 9 print("Hello") # 2个空格,不推荐 续行对齐 1# 方法1:对齐左括号 2result = some_function(argument1, argument2, 3 argument3, argument4) 4 5# 方法2:悬挂缩进 6result = some_function( 7 argument1, argument2, 8 argument3, argument4 9) 10 11# 列表、字典的续行 12my_list = [ 13 1, 2, 3, 14 4, 5, 6, 15] # 末尾逗号是好习惯 3. 空行 类和函数之间 1# 顶层函数和类之间空2行 2def function1(): 3 pass 4 5 6def function2(): 7 pass 8 9 10class MyClass: 11 pass 12 13 14class AnotherClass: 15 pass 方法之间 1class MyClass: 2 # 类中的方法之间空1行 3 def method1(self): 4 pass 5 6 def method2(self): 7 pass 函数内部逻辑分组 1def complex_function(): 2 # 初始化部分 3 x = 10 4 y = 20 5 6 # 计算部分 7 result = x + y 8 9 # 返回结果 10 return result 4. 最大行长度 每行不超过 79 个字符(文档字符串/注释不超过 72 个字符)。 ...

2024-03-02 · 4 min · 748 words · 老墨

Python教程08:列表推导式入门

Python 教程 08:列表推导式入门 “简洁是智慧的灵魂。” —— 莎士比亚 列表推导式是 Python 最具特色的语法之一,它让你用一行代码完成原本需要多行循环才能实现的功能。这不仅是代码的简化,更是思维方式的提升。 1. 什么是列表推导式 列表推导式(List Comprehension)是一种简洁的创建列表的方式。 传统方法: 1# 生成1-10的平方 2squares = [] 3for i in range(1, 11): 4 squares.append(i ** 2) 5print(squares) # [1, 4, 9, 16, ..., 100] 列表推导式: 1# 一行搞定 2squares = [i ** 2 for i in range(1, 11)] 3print(squares) # [1, 4, 9, 16, ..., 100] 代码从 3 行变成 1 行,清晰简洁,这就是 Python 的魅力。 2. 基本语法 1[表达式 for 变量 in 序列] 执行过程: 遍历序列中的每个元素 将元素赋值给变量 计算表达式 将结果添加到新列表 1# 示例 2numbers = [1, 2, 3, 4, 5] 3 4# 每个数乘以2 5doubled = [n * 2 for n in numbers] 6print(doubled) # [2, 4, 6, 8, 10] 7 8# 转换为字符串 9str_list = [str(n) for n in numbers] 10print(str_list) # ['1', '2', '3', '4', '5'] 11 12# 调用方法 13names = ['alice', 'bob', 'charlie'] 14capitalized = [name.capitalize() for name in names] 15print(capitalized) # ['Alice', 'Bob', 'Charlie'] 3. 带条件的列表推导式 可以添加 if 条件进行过滤: ...

2024-02-28 · 4 min · 816 words · 老墨

Python教程07:字符串深入

Python 教程 07:字符串深入 “语言是思维的外壳。” 字符串是编程中最常用的数据类型之一,几乎每个程序都要处理文本。今天我们深入学习 Python 字符串的各种操作,从格式化到正则表达式,让你处理文本得心应手。 1. 字符串的创建 Python 中创建字符串有多种方式: 1# 单引号 2s1 = 'Hello' 3 4# 双引号 5s2 = "World" 6 7# 三引号(多行字符串) 8s3 = """这是一个 9多行 10字符串""" 11 12s4 = '''也可以用 13单引号''' 14 15# 原始字符串(忽略转义字符) 16path = r"C:\Users\name\documents" # \n不会被解释为换行 17 18# 字符串拼接 19full = s1 + " " + s2 # "Hello World" 2. 字符串格式化:三种武器 方法 1:%格式化(老式,不推荐) 1name = "张三" 2age = 25 3print("我叫%s,今年%d岁" % (name, age)) 4 5# 格式控制 6pi = 3.14159 7print("π = %.2f" % pi) # 保留2位小数 方法 2:format()方法 1# 位置参数 2print("{}+{}={}".format(1, 2, 3)) 3 4# 索引 5print("{0}+{1}={2}".format(1, 2, 3)) 6print("{2}+{1}={0}".format(3, 2, 1)) # 调换顺序 7 8# 关键字参数 9print("{name}今年{age}岁".format(name="李四", age=30)) 10 11# 格式控制 12print("{:.2f}".format(3.14159)) # 3.14 13print("{:0>5}".format(42)) # 00042(左侧填充0,总宽度5) 14print("{:*^10}".format("Hi")) # ****Hi****(居中,宽度10,填充*) 方法 3:f-string(Python 3.6+,最推荐) 1name = "王五" 2age = 28 3city = "北京" 4 5# 简洁直观 6print(f"{name}今年{age}岁,来自{city}") 7 8# 表达式 9print(f"明年我{age + 1}岁") 10print(f"2的10次方是{2 ** 10}") 11 12# 格式控制 13pi = 3.14159 14print(f"π ≈ {pi:.2f}") 15 16# 对齐和填充 17num = 42 18print(f"{num:0>5}") # 00042 19print(f"{num:*^10}") # ****42**** 20 21# 调试输出(Python 3.8+) 22x = 10 23print(f"{x=}") # x=10 推荐:新代码统一使用 f-string,简洁且高效。 ...

2024-02-27 · 3 min · 622 words · 老墨

Python教程06:控制流程-循环语句

Python 教程 06:控制流程-循环语句 “重复是力量之母。” 如果说条件语句让程序会"选择",那循环语句就让程序会"重复"。想象一下,如果要打印 1 到 100 的数字,难道要写 100 行print()吗?循环语句就是为了解决这类重复性工作而生的。 1. for 循环:遍历序列 for 循环用于遍历序列(列表、字符串、范围等)中的每个元素。 基本语法 1# 遍历列表 2fruits = ["苹果", "香蕉", "橙子"] 3for fruit in fruits: 4 print(f"我喜欢吃{fruit}") 5 6# 遍历字符串 7for char in "Python": 8 print(char) 9 10# 遍历字典 11user = {"name": "张三", "age": 25, "city": "北京"} 12for key in user: 13 print(f"{key}: {user[key]}") 语法要点: for 变量 in 序列: 循环体必须缩进 每次循环,变量会依次取序列中的每个值 range()函数 range()生成数字序列,是 for 循环的好搭档。 1# range(stop):从0到stop-1 2for i in range(5): 3 print(i) # 0, 1, 2, 3, 4 4 5# range(start, stop):从start到stop-1 6for i in range(1, 6): 7 print(i) # 1, 2, 3, 4, 5 8 9# range(start, stop, step):指定步长 10for i in range(0, 10, 2): 11 print(i) # 0, 2, 4, 6, 8 12 13# 倒序 14for i in range(10, 0, -1): 15 print(i) # 10, 9, 8, ..., 1 enumerate():带索引的遍历 有时候需要同时获取元素和索引: ...

2024-01-21 · 4 min · 687 words · 老墨

ios如何同步obsidian笔记仓库?

对于技术人而言,经常需要记录、整理大量的笔记内容,形成自己的知识库。一款好的笔记软件我认为需要具备以下几个条件: 必须:跨平台,同时支持桌面电脑(Windows,Mac,Linux)和手机(Android,iOS) 必须:支持同步,在多台设备中打开任何一台都能接接着编写笔记 必须:实时存储,就算突然断电、司机也不会丢失已写笔记内容 必须:支持代码高亮,更便于友好地阅读代码 必须:支持 Markdown 格式,快速编写文档必备格式,谁用谁知道 必须:支持多重备份,最好是本地一份、远端一份,首选支持git同步的 可选:支持双向链接,这样笔记与笔记之间就可以形成关联关系,慢慢积累后就形成了自己的知识库 可选:支持笔记导出,比如导出 pdf 等格式,便于分享,如果能一键发布到常见博客如 hexo、wordpress、jekyll 更好 我用过诸多笔记软件,但都存在或多或少的问题,无法满足上述要求,后来一直使用网易的有道笔记,它支持一键保存笔记,不过编辑器实在难用,markdown 的图片要自己搭建图床。 阮一峰推荐的笔记软件是 github.dev,可以看看他的这篇文章。不过我更喜欢原生的 app 软件,我选择 obsidian,因为它免费而且支持上述的大部分需求,尤其是第 6 点,对于不想将笔记存储保存给笔记软件厂商而言非常好,直接使用git同步。 现在,我基本上都是用 obsidian 来记笔记和写文章,它支持双向链接,很容易形成知识体系,而且具备丰富的插件支持,具体特性可以自己咨询网络。如果对于格式非常多的文章,我也会使用 AsciiDoc 格式来编写,相比于 Markdown 它更加强大,但是语法也更复杂。Obsidian 目前并不支持,非常遗憾。 在使用 Obsidian 时,最大的问题就是手机端的同步。我是用 Github 存储笔记,手机端没有很好的同步方案,官方的同步方式无法满足需求,而且需要付费。我的需求是,macOS、windows、iPhone、iPad 四种设备上需要从 github 同步我的笔记,没有安卓端同步的需要,所以我使用 iSH 这个 app,它开源免费,支持 ios,完美的解决了我的问题。 什么是 iSH 它是一个在 ios 上模拟 Linux 环境的 app,使用的是 alpine linux,在你的 iPhone、iPad 上都可以运行并创建 Linux Shell 环境。正好我可以使用它来同步 git 仓库到我的 ios 设备上。 安装 ish app 从 ios 应用商店下载 iSH Shell,注意看名称和 logo: ...

2024-01-06 · 2 min · 366 words · 老墨

2023年终回顾和2024新年展望

每至年终,总慨时光飞逝,悔不该浪费光阴而不自知。欲提笔总结一年之种种,却感人生多琐事、岁月少芳华,生活不过柴米油盐酱醋茶。常人者,虽一年之大事鲜有,却亦欲略举一二。 2023年,整个国际形势依旧不太妙,俄乌冲突、巴以冲突、缅甸内战,各国都在想法设法为自己牟利,战乱频发,新冠疫情之后本来就衰弱的国际经济形势更是雪上加霜。逐年变暖的气候,给我们的家园带来了严峻的挑战:火灾、洪水、泥石流、地震、台风…… 2023国际十大新闻 2023中国十大新闻 作为一介庶民,本该独善其身,”各人自扫门前雪,莫管他人网上霜“,然人是社会人,践行之何其难也! 博客 这一年,撰写的文章倒是很少。一方面,感觉没什么可写的,可能自己正慢慢变得懒散了;另一方面,每每要提笔写些什么,发现对要写的东西还了解的不够透彻,只好搁浅。新的一年,得试着多见多听多写,多思考。这是本年的文章列表: 又一个大佬辞世,技术人该何去何从? Redis集群中的 CROSSSLOT Keys Error Nginx Bad Gateway和no live upstreams错误分析 依赖注入库wire使用入门 使用cli框架开发CLI程序 (译)Go1.13中处理错误 (译)Go错误是值 (译)Go模块:管理依赖项 (译)Go 中的字符串、字节、符文和字符 (译)数组、切片和字符串 - “append” 原理 技能 2023除了继续 golang 之外,也复习了一下 python3,包括: 阅读了《Python编程 从入门到实践 第2版》,里边基础讲解不是很透彻,实例倒是不错 廖雪峰的Python教程 语言都是互通的,但建议技术人至少掌握一门静态语言(Java、Go、Rust等)和一门动态脚本语言(Python,Ruby等),以便应对不同的场景,比如使用脚本语言快速开发一些小工具。2024年的工作中应该会大量使用 Go 和 Python。 2024打算加强区块链的课程,包括: 北大肖臻区块链课程 精读《精通以太坊》,之前只是泛读了一遍 精读《精通比特币》 读书是充实自我、提升自我最好的方式。 2023年读了几本书: 《Go语言精进之路1》:掌握go语言编程思想的必要书籍,尤其是从 java 转 go,很多套路与 java 不同,参考本书可以快速掌握 go 的一些编程技巧,本书还有下册,准备2024研究 《朱元璋传》:描写朱元璋的一生,讲述其如何从一个被逼进入寺庙混口饭吃的和尚,到建国当皇帝开辟传奇道路。本书内容较多,适合泛读。 《任正非传》:讲述了任正非与华为的故事,想要了解华为、了解任正非的人必读。 《苏东坡传》:林语堂的作品,讲述大才苏东坡传奇的人生故事。读此书除了了解苏轼一生大才却屡遭贬谪、坎坷的一生,更应该学习其豁达、乐观的人生境界。 《乡土中国》:目前正在读,看费孝通用浅显的文字讲述什么是中国乡土社会。 2024年打算读这几本书: ...

2024-01-02 · 1 min · 77 words · 老墨

又一个大佬辞世,技术人该何去何从?

陈皓,网名左耳朵耗子,技术圈亲切地称他为“耗子叔”、“皓哥”,是一位资深技术大咖、骨灰级程序员,前阿里云资深架构师、天猫开发总监、亚马逊高级研发经理、汤森路透基础架构师和高级研发经理,他的博客酷壳每篇文章的阅读量高达数十万,其发布的专栏课程购买量也近20万,可见其影响力惊人! 然而,这位技术大佬在2023年5月13日因心梗而与世长辞,享年47岁。英年早逝,无不令听者痛心、见者流泪! 我很早都在拜读他的文章了,之前他一直在csdn撰写了大量的技术分享类博文,后来又自建博客。其博文范围广泛,涉及架构、技术、管理、生活方方面面,其文风犀利、见解独到、个性鲜明,每每读之均令人深受启发。这位技术传道者除了爱写作、爱分享,而且还一直处于不断学习、提升自己的状态,其生前博客还发表着GoLang、Rust、区块链等等技术的文章,实乃吾辈之楷模! 有段时间没有翻阅老师的博客了,昨天一打开发现老师并没有更新文章,最近的一篇仍然停留在《是微服务架构不香还是云不香?》 翻阅博客之时,偶然看到评论中说老师此时的消息,难以置信,无比痛心,不得不感叹命运多变、世事无常。 作为技术人的我们,用技术改变着这个世界,却无法避免被这个世界所抛弃。一方面,技术人要被公司体制所折磨,公司的利益高于一切,技术只为公司的利益服务,只要有利可图,随时可以要求技术人加班加点干活,而不管需求是否合理、技术是否可达;另一方面,技术人大多是一个隐藏于后台的微小角色,他们不受重视、不被尊重,肩负着强于身体承受能力数倍的工作量,却因为一些技术问题被公司批评,甚至辞退。加班、熬夜、饮食不规律、运动少、职业病、亚健康等等词汇都是贴在每一个技术人身上的标签,年轻时不断地过度消耗着身体,殊不知随着年龄的增长,这些“负债”总是要一点点偿还,甚至是付出生命的代价。 正如陈皓在其痛恨手册中所述:“ 痛恨各种不从研发团队出发,不从团队和项目出发的流程、方法论、咨询师、SQA、流程部门。 痛恨那些为所欲为的,为了自己商业目标牺牲用户利益的中国IT企业。 痛恨中国的C2C式的那种简单的抄袭和复制。 ……” 在这喧杂的社会,我们不得不沉下心来思考:我们会不会是下一个陈皓?我们又该何去何从呢?推荐大家阅读《软技能:代码之外的生存指南》一书,该书从职业规划、自我营销、自我学习提升个人专业技能,到理财、健身、精神来修炼自身体质等多个层面向我们展示了技术人应该如何进行技能、身心双修。 这里大致罗列作者的观点,值得我们深思: 职业:每一位技术人都需要学会管理自己的职业生涯。树立明确的目标,指定可靠、周密的计划,什么时候做什么都经过精心的安排,然后朝着目标努力。 自我营销:你的营销手段决定了你的营销对象是受益还是受损。营销需要人们的关 注,以便让人们关注你,关注你的产品。优秀的营销会将人们的需要或者期待与能够满足此愿望的产品或服务关联起来。“实现价值在先,要求回报在后”。做自己的产品、写博客、发视频,让跟多的人认识你。 学习:“活到老,学到老”。尤其是技术人,在这个飞速变化的世界里,学习的能力是至关重要的。技术人绝不能固步自封,忽视自己的技能发展,否则就会被社会淘汰。 生产力:“外行静坐等待灵感,其他人则唤起激情努力工作”。如何克服拖延症,“做该做的事”,我们需要运用方法、使用工具、培养习惯,提升自己的生产力。 理财:技术人耗费青春拿到较高的薪酬,但是如果不会理财,纵使有万贯家财,最终也可能毁于一旦。 健身:“身体是革命的本钱”,长期的久坐令技术人大多处于亚健康的状态,我们需要健身,只有强健的身体才能更好的发展自己。我认为这是大部分技术人忽略的一项,但它确实重中之重! 精神:我们并不是简单的机器——我们是人类。我们不只是一个与思想相连的躯壳。我们不能只下达指示然后就期望身体能完成这些指令。这个世界存在着另一股很强大的力量,它能带领我们走 上成功之路,把我们推向成功。这种力量为——精神。身体强健了,还要求我们保持良好的精神,乐观、豁达的心态,积极上下的生活态度,都是技术人所必须的。我们不只是只在电脑上敲敲打打的码农,我们更是父母、儿女、丈夫和妻子,积极地融入生活、享受生活吧。 陈皓老师走了,仍然有无数个陈皓前仆后继地为技术而奋斗着。我们执着与技术的同时,别忘了我们仍然是这个社会的一份子,更需要积极、乐观的去享受生活的美好! 谨以此文悼念陈皓老师,一路走好!

2023-12-27 · 1 min · 24 words · 老墨

Python教程05:控制流程-条件语句

Python 教程 05:控制流程-条件语句 “人生处处是选择。” 程序和人生一样,也需要做出选择。条件语句就是让程序具备"决策"能力的工具,就像十字路口的红绿灯,告诉你该往哪走。 1. if 语句:单向选择 最简单的条件语句,满足条件就执行,不满足就跳过。 1age = 20 2 3if age >= 18: 4 print("你已经成年了") 5 print("可以独立做决定") 语法要点: if后面跟条件表达式,以冒号结尾 条件代码块必须缩进(通常 4 个空格) 缩进的代码属于 if 块,一起执行或跳过 2. if-else:双向选择 两条路,必须选一条。 1age = 15 2 3if age >= 18: 4 print("成年人,可以投票") 5else: 6 print("未成年,不能投票") 就像走到岔路口,往左或往右,总要选一个方向。 3. if-elif-else:多向选择 当选择超过两个时,使用elif(else if 的缩写)。 1score = 85 2 3if score >= 90: 4 grade = "A" 5elif score >= 80: 6 grade = "B" 7elif score >= 70: 8 grade = "C" 9elif score >= 60: 10 grade = "D" 11else: 12 grade = "F" 13 14print(f"分数:{score},等级:{grade}") 执行顺序: 从上到下依次判断 遇到第一个为 True 的条件就执行,然后跳出整个 if-elif-else 结构 如果所有条件都是 False,执行 else 块(如果有的话) 这就像走迷宫,找到第一个出口就出去了,不会继续找其他出口。 ...

2023-12-06 · 3 min · 566 words · 老墨

Python教程04:运算符

Python 教程 04:运算符 “巧妇难为无米之炊。” 有了数据类型,接下来就要学会如何操作这些数据。运算符就是操作数据的工具,就像厨房里的刀、铲、勺,每种工具各有用途。 1. 算术运算符 最基础的运算符,用于数学计算。 基本运算 运算符 说明 示例 结果 + 加法 5 + 3 8 - 减法 5 - 3 2 * 乘法 5 * 3 15 / 除法 5 / 2 2.5 // 整除 5 // 2 2 % 取模(余数) 5 % 2 1 ** 乘方 2 ** 3 8 1# 算术运算示例 2a = 10 3b = 3 4 5print(f"{a} + {b} = {a + b}") # 13 6print(f"{a} - {b} = {a - b}") # 7 7print(f"{a} * {b} = {a * b}") # 30 8print(f"{a} / {b} = {a / b}") # 3.333... 9print(f"{a} // {b} = {a // b}") # 3 10print(f"{a} % {b} = {a % b}") # 1 11print(f"{a} ** {b} = {a ** b}") # 1000 有趣的细节 除法的"历史遗留问题" ...

2023-12-04 · 5 min · 878 words · 老墨

Python教程03:数据类型基础

Python 教程 03:数据类型基础 “万物皆有类。” 在 Python 的世界里,所有数据都有自己的类型。了解数据类型,就像认识食材,知道哪些能一起烹饪,哪些会"水火不容"。 1. Python 的基础数据类型 Python 有几种基础数据类型,今天我们先学习最常用的四种: 类型 英文名 示例 说明 整数 int 42, -100, 0 没有小数点的数字 浮点数 float 3.14, -0.5, 2.0 带小数点的数字 字符串 str "Hello", 'Python' 文本数据 布尔值 bool True, False 真或假 还有一个特殊的值:None,表示"空"或"无值"。 2. 整数(int) 整数就是没有小数部分的数字,可正可负可为零。 1# 整数示例 2age = 25 3temperature = -10 4zero = 0 5 6print(age, type(age)) # 25 <class 'int'> 7print(temperature) # -10 8 9# Python 3的整数可以无限大(只要内存够) 10big_number = 1234567890123456789012345678901234567890 11print(big_number) # 正常输出,不会溢出 12 13# 不同进制的整数 14binary = 0b1010 # 二进制,等于十进制的10 15octal = 0o12 # 八进制,等于十进制的10 16hexadecimal = 0x1F # 十六进制,等于十进制的31 17print(binary, octal, hexadecimal) # 10 10 31 整数运算 1a = 10 2b = 3 3 4# 基本运算 5print(a + b) # 13 加法 6print(a - b) # 7 减法 7print(a * b) # 30 乘法 8 9# 除法:注意Python 3的除法很特别 10print(a / b) # 3.3333... 除法,结果是浮点数 11print(a // b) # 3 整除,结果是整数 12print(a % b) # 1 取模(求余数) 13 14# 乘方 15print(a ** 2) # 100 (10的2次方) 16print(2 ** 10) # 1024 这里有个有趣的现象:在 Python 3 中,10 / 3的结果是3.333...(浮点数),而不是像 Go/Java 那样得到3。如果你想要整除,必须用//。 ...

2023-11-20 · 3 min · 633 words · 老墨

Python教程02:基础语法

Python 教程 02:基础语法 “纸上得来终觉浅,绝知此事要躬行。” 学编程和学游泳一样,看再多教程也不如下水扑腾几下。今天我们来学习 Python 的基础语法,这些是写代码的"规矩"。 1. 注释:给代码写"旁白" 注释就像电影里的旁白,是写给人看的,不会被 Python 执行。写注释有两个好处: 提醒自己:三个月后回头看代码,如果没有注释,你可能会问"这是谁写的垃圾代码?"(然后发现是自己写的) 方便别人:团队协作时,注释能让其他人快速理解你的思路 单行注释 用#开头,从#开始到行尾的所有内容都是注释: 1# 这是一个单行注释 2print("Hello") # 这也是注释,可以放在代码后面 3 4# 计算圆的面积 5radius = 5 6area = 3.14 * radius ** 2 多行注释 Python 没有专门的多行注释语法,但可以用三引号('''或""")来实现: 1""" 2这是一个多行注释 3可以写很多行 4通常用于写文档字符串(docstring) 5""" 6 7''' 8单引号也可以 9但更推荐用双引号 10''' 11 12def calculate_area(radius): 13 """ 14 计算圆的面积 15 16 参数: 17 radius: 圆的半径 18 19 返回: 20 圆的面积 21 """ 22 return 3.14 * radius ** 2 最佳实践:注释要写"为什么",而不是"是什么"。比如# 计算圆的面积这种注释意义不大,因为代码本身已经很清楚了。更好的注释是:# 使用简化的π值,精确度够用且计算更快 ...

2023-11-10 · 3 min · 522 words · 老墨

Python教程01:Python简介与环境搭建

Python 教程 01:Python 简介与环境搭建 “工欲善其事,必先利其器。” 在开始学习 Python 之前,我们先要把环境搭建好。别担心,这比组装宜家家具简单多了。 1. Python 是什么? Python 是一种高级编程语言,由荷兰程序员 Guido van Rossum 于 1991 年创建。有趣的是,这个名字并非来自那条盘在树上的蟒蛇,而是源自英国喜剧团体"Monty Python"(蒙提·派森)。Guido 在开发 Python 时正在看这个喜剧团的剧集,觉得这名字"简短、独特、略带神秘",于是就用了。 不过大家还是约定俗成地用蛇做 Logo,毕竟这样看起来更酷一些。 Python 的三大特点 简单易学:语法接近自然语言,读代码就像读英文句子 功能强大:从网站开发到人工智能,从自动化脚本到科学计算,几乎无所不能 生态丰富:拥有数十万个第三方库,就像一个超级大工具箱 用一句话概括:Python 是程序员界的瑞士军刀——简单好用,功能齐全。 2. 为什么学 Python? 应用领域广泛 Web 开发:Django、Flask、FastAPI 框架让你快速搭建网站 数据分析:Pandas、NumPy、Matplotlib 是数据科学家的标配 人工智能:TensorFlow、PyTorch 支撑着深度学习的发展 自动化脚本:批量处理文件、爬虫、运维工具,让重复劳动自动化 游戏开发:Pygame 虽然小众,但也很有趣 科学计算:SciPy 在科研领域广泛应用 市场需求大 根据 TIOBE 编程语言排行榜,Python 常年稳居前三。这意味着无论你是找工作、做副业,还是只是想提升技能,Python 都是一个非常好的选择。 就像学外语,你可以学世界语(优雅但没人用),也可以学英语(实用且吃香)。Python 就是编程界的"英语"。 代码简洁优雅 Python 崇尚"用最少的代码做最多的事"。同样的功能,Python 代码往往比其他语言短 50%以上。比如打印 1 到 10: 1# Python:简洁明了 2for i in range(1, 11): 3 print(i) 非常的简洁。 3. Python 2 vs Python 3:历史的遗留问题 目前 Python 有两个主要版本: ...

2023-10-18 · 2 min · 369 words · 老墨

Redis集群中的 CROSSSLOT Keys Error

场景 Redis单节点没有问题,切换到Redis Cluster后业务上某一功能出错: CROSSSLOT Keys in request don't hash to the same slot 出错的代码: 1var ctx = context.TODO() 2_, err := uq.queueImpl.rc.TxPipelined(ctx, func(pip redis.Pipeliner) error { 3 cmd := pip.LPush(ctx, uq.key, dest...) 4 if cmd.Err() != nil { 5 return cmd.Err() 6 } 7 return pip.SAdd(context.Background(), uq.setkey, dest...).Err() 8}) 这段代码的逻辑是向 list 中push一条数据,再向一个 set 加入数据,两步操作通过 pipline 在同一个事务中完成。 问题分析 错误的大概意思就是: 请求中跨槽的key没有被hash到相同的hash槽中。通过代码分析,事务中的两次操作的key并不相同,他们没有被hash到同一个hash槽从而出现上述错误。 什么是hash槽 Redis Cluster 规范中的 Key distribution model(key分布模型)说明如下: Redis集群中将key的存储空间划分为16384个slot,整个集群可以支持最大16384个主节点,实际上建议不超过1000个节点。每一个主节点又可以处理16384个 hash slot。整个集群将hash slot分布于不同的节点组成高可用集群,单个节点有多个副本以便在节点故障时将重新平衡节点,达到高可用的目的。关于可用性保证,可以看这里。 如下的公式用于计算key的hash slot: HASH_SLOT = CRC16(key) mod 16384 Redis将相同hash值的slot分布到同一个node下,如下图所示: 图片出自这里 可以看出,hash槽(slot)就是一个整数,通过key计算得来,它的作用就是决定key存储于哪一个节点中。 为什么会出现CROSSSLOT错误 主要原因是应用程序尝试在多个键上运行命令,但操作中涉及的键不在同一个哈希槽中,这会导致集群中不允许的“CROSSSLOT”操作。 比如,使用Set的SUION命令时,如果多个key的 hash slot 不在集群中的同一个node上,则会出现CROSSSLOT错误。 前边场景中,在同一个事务中操作多个key,集群环境下必须要保证这些被操作的key必须被hash到同一个slot,否则同样会抛出CROSSSLOT错误。 Redis这么做的主要原因还是在于避免分布式数据被破坏的风险,而且在同一个事务下或者同一个命令中操作多个跨节点的key,会因网络等因素带来性能损耗,所以Redis禁止这么做。如果有这种场景,Redis也提供了解决方案:使用Hash Tags。 问题解决 那么,什么是 Hash Tag? ...

2023-10-17 · 1 min · 128 words · 老墨

Nginx Bad Gateway和no live upstreams错误分析

最近项目的生产环境中客户端出现大量的Nginx 502 Bad Gateway错误,逐步排查最终定位到是由于被ddos攻击造成服务器资源耗尽无法响应造成的问题。遂整理过程著文以记之。 场景 线上4个节点,每个节点都有两个相同服务通过Nginx作负载均衡,均采用Nginx默认值,未做过多配置,配置类似: 1upstream test-server { 2 server 127.0.0.1:8002; 3 server 127.0.0.1:8001; 4} 客户端出现大量的 502 Bad Gateway 信息,查看Nginx错误日志,信息如下: no live upstreams while connecting to upstream 初步定位问题,发现后台出现了很多莫名其妙的错误,查看Nginx错误日志发现也打印了很多上述错误。怀疑是后台某个接口请求出错导致返回了 500,导致Nginx将这个服务下线,后经过排查,后台确实存在一些错误信息,但是出错上述错误的时间不匹配。 后整理思路并认真思考,发现可能思路存在偏差:后台http状态码怎么会影响Nginx将服务下线呢? 因为Http状态码表示http响应的状态,也就是表示响应结果的正确与否,比如 2xx 表示服务端正确处理并响应了数据,4xx 表示客户端存在错误妨碍了服务端的处理,5xx 表示服务端错误导致处理失败了。但是,能够返回这些状态码说明服务端连接正常,只是由于特定原因导致服务端响应错误了。返回了这些状态码Nginx当真会将服务下线吗?试想一下,某一个客户端请求查询一条不存在的数据导致服务端处理时抛出异常并响应 500 状态码,然后Nginx认为服务不可用并将其下线,导致一定时间内所有的请求都返回上述错误,那么罪魁祸首到底是服务端还是客户端?Nginx这么处理是不是太过了呢?Nginx肯定不会这么来设计。 所以,我猜测Nginx不可能如此是非不分,应该是别的原因导致的。 查阅upstream官方文档,看到这么两个参数配置: fail_timeout=time: 设置多少时间内不能与下游服务成功通信 max_fails 次后可以认为下游服务不可用,这是一个周期时间,即每隔 fail_timeout 时间都会进行统计,默认为 10 秒。 max_fails=number: 设置在 fail_timeout 时间内与下游服务通信失败的次数,达到该次数后认为服务不可用,默认为1。 按照Nginx的默认设置,也就是说每个10秒统计下有服务的通信失败次数,达到1次就认为服务不可用,此时Ngingx会将其踢下线,后续所有转发到该服务的请求都会返回 no live upstreams while connecting to upstream,直到下一个10秒再重新处理(max_fails 为0又重新将服务上线)。 关键在于这个 通信失败 的理解。通信失败,表示Nginx转发请求给服务,但是服务没有任何响应,而不是一开始怀疑的 HTTP 状态不是200,能成功响应,不论是什么状态码,都应该认为与服务通信成功。 实践才能出真知,为了验证我的猜想,必须进行实验。 实验 使用golang编写一个http服务,代码如下: 1var inErr bool 2func main() { 3 port := os.Args[1] 4 node := os.Args[2] 5 r := gin.Default() 6 go func() { 7 for { 8 if node == "node1" { 9 break 10 } 11 inErr = !inErr 12 time.Sleep(3 * time.Second) 13 } 14 }() 15 r.GET("/", func(ctx *gin.Context) { 16 if inErr { 17 ctx.String(http.StatusInternalServerError, "error: "+node) 18 } else { 19 ctx.JSON(http.StatusOK, gin.H{"msg": "ok, " + node}) 20 } 21 }) 22 r.GET("/err", func(ctx *gin.Context) { 23 ctx.String(http.StatusInternalServerError, "error: "+node) 24 ctx.Abort() 25 }) 26 r.GET("/timeout", func(ctx *gin.Context) { 27 time.Sleep(time.Second * 10) 28 ctx.String(http.StatusOK, "after 10s responsed") 29 }) 30 _ = r.Run(":" + port) // 参数0为执行文件本身信息,真正的参数下标为1 31} 上述代码使用了 gin 框架,大致的逻辑: ...

2023-10-07 · 5 min · 922 words · 老墨