[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" 要点: ...

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

Rust 学习笔记 07:Slice 类型

Rust 学习笔记 07:Slice 类型 “Slice and dice your data safely.” 在 Go 语言中,Slice (切片) 是一个非常核心的概念,它底层是一个结构体 (ptr, len, cap)。 在 Rust 中,Slice 也是类似的,但它有一个本质的区别:Rust 的 Slice 是一种引用(Borrowed Type)。 它不拥有数据,它只是借用了数据的一部分。 1. 字符串切片 (String Slices) 假设我们有一个字符串 s: 1let s = String::from("hello world"); 我们可以创建一个切片,指向它的一部分: 1let hello = &s[0..5]; // 指向 0,1,2,3,4 2let world = &s[6..11]; 注意那个 & 符号,说明 hello 是一个引用。 它的类型是 &str(读作 “string slice”)。它内部包含两个字段: 指向数据的指针 (ptr) 切片的长度 (len) 这和 Go 的 Slice 结构几乎一样,只是少了容量 (cap),且必须依附于原字符串存在。 2. 切片与所有权 既然 Slice 是引用,它就受借用规则的约束。 ...

2024-09-08 · 2 min · 217 words · 老墨

Python教程20:模块基础

Python教程20:模块基础 “分而治之,事半功倍。” 当代码越来越多时,把所有代码放在一个文件里会变得难以维护。模块(Module)就是Python的代码组织方式,让你可以把相关功能分门别类。今天我们学习如何创建和使用模块。 1. 什么是模块 定义 **模块(Module)**是一个包含Python代码的.py文件。每个Python文件都可以作为一个模块被其他文件导入使用。 为什么需要模块: 代码复用:写一次,到处用 命名空间:避免变量名冲突 代码组织:按功能分类,便于维护 协作开发:不同人负责不同模块 简单示例 创建一个文件math_utils.py: 1# math_utils.py 2"""数学工具模块""" 3 4PI = 3.14159 5 6def add(a, b): 7 """加法""" 8 return a + b 9 10def circle_area(radius): 11 """计算圆面积""" 12 return PI * radius ** 2 13 14class Calculator: 15 """计算器类""" 16 def multiply(self, a, b): 17 return a * b 在另一个文件中使用: 1# main.py 2import math_utils # 导入模块 3 4print(math_utils.PI) # 3.14159 5print(math_utils.add(3, 5)) # 8 6print(math_utils.circle_area(5)) # 78.53975 7 8calc = math_utils.Calculator() 9print(calc.multiply(4, 5)) # 20 模块的本质: 模块是一个命名空间(namespace) 文件名(去掉.py)就是模块名 通过模块名访问其中的变量、函数、类 2. 导入模块的方式 Python提供了多种导入方式,适应不同场景。 方式1:import模块名 1import math_utils 2 3# 使用:模块名.成员 4result = math_utils.add(1, 2) 优点: ...

2024-09-06 · 3 min · 554 words · 老墨

Rust 学习笔记 06:引用与借用 (References & Borrowing) 下

Rust 学习笔记 06:引用与借用 (References & Borrowing) 下 “Borrow checkers are the strict librarians of the programming world.” 上一集我们讲了所有权 (Ownership):把东西给别人(Move),自己就没了。这虽然安全,但太麻烦了。 比如我想计算一个字符串长度,难道每次都要把所有权传进去,算完再传回来? 1// 痛苦的写法 2fn calculate_length(s: String) -> (String, usize) { 3 let length = s.len(); 4 (s, length) // 还要把 s 传回去,心累 5} Rust 说:别急,你可以 借 (Borrow) 啊。 1. 引用 (References) 引用允许你使用值但不获取其所有权。在 Rust 中,用 & 符号表示。 1fn main() { 2 let s1 = String::from("hello"); 3 4 // 传引用:&s1 5 let len = calculate_length(&s1); 6 7 // s1 依然有效!因为我们只是借出去了,没送出去。 8 println!("The length of '{}' is {}.", s1, len); 9} 10 11// 接收引用:&String 12fn calculate_length(s: &String) -> usize { 13 s.len() 14} 这叫 借用 (Borrowing)。就像你借了朋友的书,你看完了得还回去(离开作用域),但书还是朋友的。 ...

2024-08-25 · 2 min · 278 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 · 老墨

Python教程19:装饰器入门

Python教程19:装饰器入门 “简洁的力量在于抽象的恰当。” 装饰器是Python最强大的特性之一,也是初学者觉得最"神秘"的部分。今天我们从基础开始,逐步揭开装饰器的面纱,理解它的本质和应用。 1. 什么是装饰器 问题场景 假设你有多个函数,需要在每个函数执行前后记录日志: 1def add(a, b): 2 print("函数开始执行") # 重复代码 3 result = a + b 4 print("函数执行完毕") # 重复代码 5 return result 6 7def multiply(a, b): 8 print("函数开始执行") # 重复代码 9 result = a * b 10 print("函数执行完毕") # 重复代码 11 return result 问题:每个函数都要写重复的日志代码,违反了DRY(Don’t Repeat Yourself)原则。 装饰器的解决方案 装饰器是一种设计模式,用于在不修改原函数代码的情况下,给函数添加新功能。就像给礼物包装一样,外面包了一层,但礼物本身没变。 1@log_decorator # 这就是装饰器 2def add(a, b): 3 return a + b 4 5# 调用时自动有日志功能 6add(3, 5) 7# 输出: 8# 函数开始执行 9# 8 10# 函数执行完毕 2. 理解装饰器的基础:闭包 在学习装饰器前,需要先理解闭包(Closure)。 什么是闭包 闭包是指:一个函数内部定义的函数,可以访问外部函数的变量。 1def outer(x): 2 # x是外部函数的变量 3 4 def inner(y): 5 # inner可以访问outer的变量x 6 return x + y 7 8 return inner # 返回内部函数 9 10# 创建闭包 11add_5 = outer(5) 12print(add_5(3)) # 8(3 + 5) 13print(add_5(10)) # 15(10 + 5) 关键点: ...

2024-08-16 · 4 min · 775 words · 老墨

Python教程18:Lambda函数与高阶函数

Python教程18:Lambda函数与高阶函数 “简洁是智慧的灵魂。” Lambda函数是Python中一种简洁的函数定义方式,配合map、filter等高阶函数,能让代码更加优雅。今天我们学习函数式编程的基础。 1. Lambda函数基础 什么是Lambda Lambda是匿名函数,用于简单的单行函数: 1# 普通函数 2def square(x): 3 return x ** 2 4 5# Lambda函数 6square_lambda = lambda x: x ** 2 7 8print(square(5)) # 25 9print(square_lambda(5)) # 25 语法 1lambda 参数: 表达式 只能有一个表达式 自动返回表达式的值 不需要return 可以有多个参数 1# 多个参数 2add = lambda x, y: x + y 3print(add(3, 5)) # 8 4 5# 无参数 6greet = lambda: "Hello!" 7print(greet()) # Hello! 8 9# 默认参数 10power = lambda x, n=2: x ** n 11print(power(3)) # 9 12print(power(3, 3)) # 27 2. Lambda的实际应用 排序 1# 按元组第二个元素排序 2students = [ 3 ("Alice", 85), 4 ("Bob", 92), 5 ("Charlie", 78) 6] 7 8# 使用lambda 9students.sorted(key=lambda x: x[1], reverse=True) 10print(students) 11# [('Bob', 92), ('Alice', 85), ('Charlie', 78)] 12 13# 字典排序 14scores = {"Alice": 85, "Bob": 92, "Charlie": 78} 15sorted_scores = dict(sorted(scores.items(), key=lambda x: x[1], reverse=True)) 条件判断 1# 三元表达式 2max_val = lambda a, b: a if a > b else b 3print(max_val(10, 20)) # 20 4 5# 复杂条件 6grade = lambda score: 'A' if score >= 90 else ('B' if score >= 80 else 'C') 7print(grade(85)) # B 3. 高阶函数 高阶函数是接受函数作为参数或返回函数的函数。 ...

2024-08-07 · 3 min · 639 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} ⚠️ 注意:大括号 {} 是必须的,且左大括号不能换行。 ...

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

Rust 学习笔记 05:所有权 (Ownership) 上

Rust 学习笔记 05:所有权 (Ownership) 上 “I thought I knew what ownership meant until I met the borrow checker.” – Anonymous Rustacean 终于来到了 Rust 的核心 —— 所有权 (Ownership)。 作为 Go 开发者,我们习惯了 GC(垃圾回收)帮我们打理一切。我们随手创建一个指针,传给函数,传给 Channel,从来不需要关心它什么时候被释放。因为有 GC 在兜底。 但在 Rust 里,没有 GC。但它也没有让我们像 C++ 那样手动 malloc/free。 那它是怎么管理内存的? 答案就是:所有权系统 + 编译器静态检查。 1. 核心原则 所有权规则非常霸道,但只有三条: Rust 中的每一个值都有一个被称为其 所有者 (owner) 的变量。 值在任一时刻有且只有一个所有者。 当所有者(变量)离开作用域,这个值将被丢弃 (Drop),内存被释放。 这听起来很像作用域管理,但最关键的是第二条:有且只有一个所有者。 2. 移动 (Move) 语义 在 Go 中,变量赋值默认是值拷贝(对于指针是拷贝地址)。 1// Go 代码 2s1 := "hello" 3s2 := s1 4// 现在 s1 和 s2 都指向同一个字符串,随便用 但在 Rust 中,对于复杂类型(如 String,在堆上分配),情况完全不同: ...

2024-07-20 · 2 min · 274 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) 不需要某个返回值?用下划线 _ 忽略: ...

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

Python教程17:函数基础

Python 教程 17:函数基础 “代码复用,从函数开始。” 函数是编程中最重要的概念之一。如果说变量是存储数据的容器,那函数就是执行任务的工具。掌握函数,你的代码将更加模块化、可维护、可复用。 1. 什么是函数 函数是一段可重复使用的代码块,用于完成特定任务。 1# 定义函数 2def greet(): 3 print("你好,世界!") 4 5# 调用函数 6greet() # 输出:你好,世界! 7greet() # 可以多次调用 为什么需要函数: 代码复用:写一次,用多次 模块化:把复杂问题分解 可维护:修改一处,处处生效 可读性:函数名即文档 2. 函数的定义 1def 函数名(参数列表): 2 """文档字符串""" 3 函数体 4 return 返回值 关键字: def:定义函数 函数名:遵循变量命名规则 参数列表:可选 return:返回值,可选 1# 最简单的函数 2def hello(): 3 print("Hello") 4 5# 带参数的函数 6def greet(name): 7 print(f"你好,{name}!") 8 9# 带返回值的函数 10def add(a, b): 11 return a + b 12 13# 完整的函数 14def calculate_area(radius): 15 """ 16 计算圆的面积 17 18 Args: 19 radius: 圆的半径 20 21 Returns: 22 圆的面积 23 """ 24 pi = 3.14159 25 return pi * radius ** 2 3. 函数的参数 位置参数 按顺序传递: ...

2024-07-05 · 3 min · 563 words · 老墨

Python教程16:数据结构综合实战

Python 教程 16:数据结构综合实战 “纸上得来终觉浅,绝知此事要躬行。” 学习了列表、元组、字典、集合后,让我们通过一个实际项目——学生成绩管理系统,把这些知识串起来,体会如何选择和组合不同的数据结构。 1. 项目需求 开发一个学生成绩管理系统,功能包括: 添加学生信息 录入和修改成绩 查询学生成绩 统计分析(平均分、最高分等) 筛选功能(如成绩优秀的学生) 2. 数据结构设计 方案选择 1# 学生信息:用字典 2student = { 3 "id": "2024001", 4 "name": "张三", 5 "age": 18, 6 "scores": { # 成绩:嵌套字典 7 "数学": 95, 8 "英语": 88, 9 "物理": 92 10 } 11} 12 13# 所有学生:用列表存储 14students = [ 15 {"id": "2024001", "name": "张三", "age": 18, "scores": {"数学": 95}}, 16 {"id": "2024002", "name": "李四", "age": 19, "scores": {"数学": 88}}, 17] 18 19# 学号索引:用字典加速查找 20students_by_id = { 21 "2024001": students[0], 22 "2024002": students[1], 23} 3. 核心功能实现 3.1 学生管理类 1class StudentManager: 2 """学生成绩管理系统""" 3 4 def __init__(self): 5 self.students = {} # {学号: 学生信息} 6 7 def add_student(self, student_id, name, age): 8 """添加学生""" 9 if student_id in self.students: 10 print(f"学号{student_id}已存在") 11 return False 12 13 self.students[student_id] = { 14 "id": student_id, 15 "name": name, 16 "age": age, 17 "scores": {} 18 } 19 print(f"✓ 添加学生:{name}") 20 return True 21 22 def add_score(self, student_id, subject, score): 23 """添加成绩""" 24 if student_id not in self.students: 25 print(f"学号{student_id}不存在") 26 return False 27 28 self.students[student_id]["scores"][subject] = score 29 print(f"✓ 录入成绩:{subject} = {score}") 30 return True 31 32 def get_student(self, student_id): 33 """查询学生信息""" 34 return self.students.get(student_id) 35 36 def get_average(self, student_id): 37 """计算学生平均分""" 38 student = self.get_student(student_id) 39 if not student or not student["scores"]: 40 return None 41 42 scores = student["scores"].values() 43 return sum(scores) / len(scores) 44 45 def get_top_students(self, n=5): 46 """获取成绩最好的N名学生""" 47 # 计算每个学生的平均分 48 student_avgs = [] 49 for sid, student in self.students.items(): 50 if student["scores"]: 51 avg = sum(student["scores"].values()) / len(student["scores"]) 52 student_avgs.append((student["name"], avg)) 53 54 # 排序并返回前N名 55 student_avgs.sort(key=lambda x: x[1], reverse=True) 56 return student_avgs[:n] 57 58 def get_subject_stats(self, subject): 59 """统计某科目的成绩分布""" 60 scores = [] 61 for student in self.students.values(): 62 if subject in student["scores"]: 63 scores.append(student["scores"][subject]) 64 65 if not scores: 66 return None 67 68 return { 69 "count": len(scores), 70 "max": max(scores), 71 "min": min(scores), 72 "avg": sum(scores) / len(scores) 73 } 74 75 def find_by_score_range(self, subject, min_score, max_score): 76 """查找某科目分数在指定范围内的学生""" 77 result = [] 78 for student in self.students.values(): 79 if subject in student["scores"]: 80 score = student["scores"][subject] 81 if min_score <= score <= max_score: 82 result.append({ 83 "name": student["name"], 84 "score": score 85 }) 86 return result 3.2 使用示例 1# 创建管理器 2manager = StudentManager() 3 4# 添加学生 5manager.add_student("2024001", "张三", 18) 6manager.add_student("2024002", "李四", 19) 7manager.add_student("2024003", "王五", 18) 8 9# 录入成绩 10manager.add_score("2024001", "数学", 95) 11manager.add_score("2024001", "英语", 88) 12manager.add_score("2024001", "物理", 92) 13 14manager.add_score("2024002", "数学", 87) 15manager.add_score("2024002", "英语", 94) 16manager.add_score("2024002", "物理", 89) 17 18manager.add_score("2024003", "数学", 92) 19manager.add_score("2024003", "英语", 85) 20manager.add_score("2024003", "物理", 95) 21 22# 查询学生 23student = manager.get_student("2024001") 24print(f"\n学生信息:{student['name']}") 25print(f"成绩:{student['scores']}") 26 27# 计算平均分 28avg = manager.get_average("2024001") 29print(f"平均分:{avg:.2f}") 30 31# 获取前3名 32print("\n前3名学生:") 33top_students = manager.get_top_students(3) 34for i, (name, avg) in enumerate(top_students, 1): 35 print(f"{i}. {name}: {avg:.2f}") 36 37# 统计数学成绩 38print("\n数学成绩统计:") 39stats = manager.get_subject_stats("数学") 40print(f"人数:{stats['count']}") 41print(f"最高分:{stats['max']}") 42print(f"最低分:{stats['min']}") 43print(f"平均分:{stats['avg']:.2f}") 44 45# 查找数学成绩90分以上的学生 46print("\n数学90分以上:") 47excellent = manager.find_by_score_range("数学", 90, 100) 48for item in excellent: 49 print(f"{item['name']}: {item['score']}") 4. 数据结构选择分析 为什么用字典存储学生 1# 方案1:列表(不推荐) 2students_list = [...] 3# 查找学生需要遍历:O(n) 4 5# 方案2:字典(推荐) 6students_dict = {"2024001": {...}} 7# 查找学生直接访问:O(1) 选择依据:需要频繁通过学号查找学生,字典更高效。 ...

2024-07-01 · 3 min · 633 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。 让我们从一个在两个银行账户之间转移资金的简单函数开始: 1transfer1 (amount, account_from, account_to) { 2 if (account_from.balance < amount) return NOPE; 3 account_to.balance += amount; 4 account_from.balance -= amount; 5 return YEP; 6} 当然,这并不是银行真正转移资金的方式,但这个例子非常有用。我们知道,账户余额应该是非负的,并且转移之后不能凭空创造(多出)或损失(丢失)金钱。当在没有外部同步的情况下从多个线程调用时,该函数会产生 Data Race(多个线程可以同时尝试更新帐户余额)和 Race Condition(在并行上下文中它将创造或损失金钱)。 我们可以尝试这样修复它: ...

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

Rust 学习笔记 04:控制流程

Rust 学习笔记 04:控制流程 “Controlling complexity is the essence of computer programming.” – Brian Kernighan 写控制流程(条件判断、循环)是程序员每天都在做的事。 对于 Go 开发者来说,我们习惯了 if err != nil 和万能的 for 循环。 但在 Rust 里,控制流程被注入了"表达式"的灵魂,变得更加灵活(也更骚)。 1. if 表达式 注意标题:是 if 表达式,不是 if 语句。这意味着 if 结构本身可以产生一个值。 基础用法 和 Go 一样,Rust 的 if 条件不需要括号 (),但执行体必须用大括号 {}。 1let number = 3; 2if number < 5 { 3 println!("condition was true"); 4} else { 5 println!("condition was false"); 6} 作为表达式赋值 这是 Go 做不到的。因为 if 是表达式,我们可以把它放在 let 语句的右边。这完全替代了 Java/C++ 中的三元运算符 ? :。 ...

2024-06-15 · 2 min · 307 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,在上一篇已经介绍过了。 ...

2024-06-10 · 4 min · 740 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,所以需要手动安装。 1sudo apt update 2sudo apt install snapd 测试是否安装成功,可以安装官方的 hello-world 程序: 1$ snap install hello-world 22024-06-09T16:15:39+08:00 INFO Waiting for automatic snapd restart... 3hello-world 6.4 from Canonical✓ installed 执行并成功输出信息表示安装成功: 1$ hello-world 2Hello 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 · 377 words · 老墨

Python教程15:集合

Python 教程 15:集合 “世界上没有两片完全相同的树叶。” 集合(Set)是 Python 中一个特殊的数据结构,它最大的特点是元素唯一、无序。就像数学中的集合概念,非常适合去重和集合运算。 1. 什么是集合 集合是一个无序的、不重复的元素集合。 1# 创建集合 2empty_set = set() # 空集合(注意不是{}) 3numbers = {1, 2, 3, 4, 5} 4mixed = {1, "hello", 3.14, True} 5 6# 使用set()函数 7from_list = set([1, 2, 2, 3, 3, 3]) # {1, 2, 3}(自动去重) 8from_string = set("hello") # {'h', 'e', 'l', 'o'} 9 10# 集合推导式 11squares = {x**2 for x in range(5)} # {0, 1, 4, 9, 16} 注意:{}是空字典,不是空集合! 1empty_dict = {} 2empty_set = set() 3 4print(type(empty_dict)) # <class 'dict'> 5print(type(empty_set)) # <class 'set'> 2. 集合的基本操作 1fruits = {"苹果", "香蕉", "橙子"} 2 3# 添加元素 4fruits.add("葡萄") 5print(fruits) # {'苹果', '香蕉', '橙子', '葡萄'} 6 7# 添加多个元素 8fruits.update(["西瓜", "芒果"]) 9print(fruits) 10 11# 删除元素 12fruits.remove("香蕉") # 不存在会报错 13# fruits.remove("榴莲") # KeyError 14 15fruits.discard("橙子") # 不存在不报错 16fruits.discard("榴莲") # 不会报错 17 18# pop():randomly删除并返回一个元素 19item = fruits.pop() 20print(f"删除了:{item}") 21 22# clear():清空 23fruits.clear() 3. 集合运算 集合支持数学中的集合运算: ...

2024-06-04 · 3 min · 542 words · 老墨

Python教程14:字典基础

Python 教程 14:字典基础 “一把钥匙开一把锁。” 字典(Dictionary)是 Python 中最灵活、最强大的数据结构之一。它通过"键-值对"的方式存储数据,就像真实世界的字典用单词查释义一样。 1. 什么是字典 字典是一个无序的、可变的键值对集合。 1# 创建字典 2empty_dict = {} # 空字典 3user = { 4 "name": "张三", 5 "age": 25, 6 "city": "北京" 7} 8 9# 使用dict()函数 10from_pairs = dict([("a", 1), ("b", 2)]) 11from_keywords = dict(name="李四", age=30) 12 13# 字典推导式 14squares = {x: x**2 for x in range(5)} 15# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} 2. 访问字典 1user = {"name": "张三", "age": 25, "city": "北京"} 2 3# 方法1:[]访问 4print(user["name"]) # 张三 5# print(user["phone"]) # KeyError: 键不存在会报错 6 7# 方法2:get()方法(推荐) 8print(user.get("name")) # 张三 9print(user.get("phone")) # None(不报错) 10print(user.get("phone", "未设置")) # 未设置(默认值) 11 12# 检查键是否存在 13print("name" in user) # True 14print("phone" in user) # False 15print("phone" not in user) # True 3. 修改字典 1user = {"name": "张三", "age": 25} 2 3# 修改值 4user["age"] = 26 5print(user) # {'name': '张三', 'age': 26} 6 7# 添加键值对 8user["city"] = "上海" 9print(user) # {'name': '张三', 'age': 26, 'city': '上海'} 10 11# 删除键值对 12del user["city"] 13print(user) # {'name': '张三', 'age': 26} 14 15# pop():删除并返回值 16age = user.pop("age") 17print(age) # 26 18print(user) # {'name': '张三'} 19 20# popitem():删除并返回最后一个键值对(3.7+有序) 21user = {"a": 1, "b": 2, "c": 3} 22item = user.popitem() 23print(item) # ('c', 3) 4. 字典常用方法 获取键、值、项 1user = {"name": "张三", "age": 25, "city": "北京"} 2 3# keys():所有键 4print(user.keys()) # dict_keys(['name', 'age', 'city']) 5print(list(user.keys())) # ['name', 'age', 'city'] 6 7# values():所有值 8print(user.values()) # dict_values(['张三', 25, '北京']) 9print(list(user.values())) # ['张三', 25, '北京'] 10 11# items():所有键值对 12print(user.items()) 13# dict_items([('name', '张三'), ('age', 25), ('city', '北京')]) 14 15# 转换为列表 16items_list = list(user.items()) 17print(items_list) 18# [('name', '张三'), ('age', 25), ('city', '北京')] 更新和清空 1user = {"name": "张三", "age": 25} 2 3# update():更新字典 4user.update({"age": 26, "city": "北京"}) 5print(user) 6# {'name': '张三', 'age': 26, 'city': '北京'} 7 8# 也可以用关键字参数 9user.update(phone="13800138000") 10print(user) 11 12# clear():清空字典 13user.clear() 14print(user) # {} setdefault() 如果键存在,返回其值;否则设置默认值并返回: ...

2024-06-03 · 3 min · 607 words · 老墨

Python教程13:元组

Python 教程 13:元组 “不变,有时候是一种力量。” 元组和列表很像,但有一个关键区别:元组是不可变的。这个特性让元组在某些场景下比列表更安全、更高效。 1. 什么是元组 元组(Tuple)是一个有序的、不可变的元素集合。 1# 创建元组 2empty_tuple = () # 空元组 3single = (1,) # 单元素元组(注意逗号) 4numbers = (1, 2, 3, 4, 5) 5mixed = (1, "hello", 3.14, True) 6nested = ((1, 2), (3, 4)) 7 8# 也可以不用括号(但不推荐) 9point = 10, 20 10print(point) # (10, 20) 11 12# 使用tuple()函数 13from_list = tuple([1, 2, 3]) 14from_string = tuple("Python") # ('P', 'y', 't', 'h', 'o', 'n') 注意:单元素元组必须有逗号! 1not_a_tuple = (1) # 这是整数1 2is_a_tuple = (1,) # 这才是元组 2. 访问元组元素 元组的访问方式和列表完全相同: 1fruits = ("苹果", "香蕉", "橙子", "葡萄") 2 3# 索引 4print(fruits[0]) # 苹果 5print(fruits[-1]) # 葡萄 6 7# 切片 8print(fruits[1:3]) # ('香蕉', '橙子') 9print(fruits[::-1]) # ('葡萄', '橙子', '香蕉', '苹果') 10 11# 遍历 12for fruit in fruits: 13 print(fruit) 14 15# enumerate 16for i, fruit in enumerate(fruits): 17 print(f"{i}: {fruit}") 3. 元组的不可变性 元组创建后不能修改: ...

2024-05-21 · 3 min · 547 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 实例下执行,用于抑制这一组请求。 ...

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. 项目结构 1file_renamer/ 2├── file_renamer.py # 主程序 3├── renamer.py # 核心重命名逻辑 4├── utils.py # 工具函数 5└── 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 · 370 words · 老墨

Redis集群中的 CROSSSLOT Keys Error

场景 Redis单节点没有问题,切换到Redis Cluster后业务上某一功能出错: 1CROSSSLOT 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分布于不同的节点组成高可用集群,单个节点有多个副本以便在节点故障时将重新平衡节点,达到高可用的目的。关于可用性保证,可以看这里。 ...

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错误日志,信息如下: 1no 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 · 老墨

依赖注入库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} 有时我们将错误与已知的 标记 值进行比较,以查看是否发生了特定错误。 ...

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)。 1const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98" 打印字符串 由于上边我们的示例字符串 sample 中的某些字节不是有效的 ASCII,甚至不是有效的 UTF-8,所以直接打印字符串会产生奇怪的输出。简单的打印语句如下: 1fmt.Println(sample) 产生这种乱码(输出可能因环境而异)输出: 1��=� ⌘ 为了找出 sample 字符串底层到底是什么,我们需要把它拆开检查一下。有几种方法可以做到这一点。最明显的是循环其内容并单独提取字节,如以下for循环所示: 1for i := 0; i < len(sample); i++ { 2 fmt.Printf("%x ", sample[i]) // 输出为十六进制格式 3} 正如前文所述,索引字符串访问的是单个字节,而不是单个字符,我们将在下面详细阐述该主题。现在,让我们只使用字节,这是逐字节循环的十六进制输出: ...

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 会使程序崩溃。 ...

2023-02-09 · 8 min · 1616 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 · 436 words · 老墨