如何在多个编程语言间切换自如

Attention

要自由切换编程语言,必然要通过实践,编程语言之海浩瀚无边,本文会随着作者的实践而不断更新,敬请期待。

基本思路

  1. 了解各语言的历史和设计背景,都有哪些考虑从而形成了如今的语法。
  2. 熟悉各语言基本语法,怎么定义变量、函数、控制流等,这里有语法简介、数据结构等章节。
  3. 识别各语言的特性,为什么 Python 有元组等,也就是要知道语言的特性和高级用法等。

在熟悉基本语法时,要能搞清楚各编程语言怎么描述相同的功能的。 在识别语言特性时,要能搞清楚各编程语言处理方式上的设计哲学。

追溯历史

Go 与一众 C/C++/Java 语法的差异,根源是什么?

设计起源:2007 年,Google 内部 C++ 项目构建缓慢(数小时)、依赖混乱、团队协作成本高。Rob Pike(Plan 9/Limbo 作者)、Ken Thompson(Unix/B 语言作者)等亲历 C 语言生态的维护困境。

核心目标:“构建速度、依赖管理、代码可读性、并发支持、部署简单性——这些比语言特性炫技更重要。”

核心哲学:“Less is exponentially more”,克制新增特性(每个语法糖都需证明其收益远大于维护成本)、工具链友好性(go vet等)、人类优先(牺牲“写时便捷”,换取“读时清晰”)、向后兼容(“加特性易,删特性难”)。

正在加载思维导图...

以下摘抄于 Go 项目的 FAQ

我们决定退一步思考:随着技术发展,未来几年内哪些重大问题将会主导软件工程领域,而一门新语言又该如何帮助解决这些问题。例如,多核 CPU 的兴起表明,一门语言应当对某种形式的并发或并行提供原生级支持。并且,为了让大型并发程序中的资源管理变得可控,垃圾回收,或至少是某种安全的自动内存管理机制是必不可少的。

一个核心目标是,Go 语言要通过支持工具开发、自动化代码格式化等日常琐碎任务,以及消除大型代码库开发中的障碍,为一线程序员提供更多帮助。

Go 于 2009 年 11 月 10 日 正式成为公开的开源项目。

Go 语言在语法体系上主要归属于 C 语言家族(基础语法层面),同时大量借鉴了 Pascal/Modula/Oberon 家族的设计思路(如声明方式、包管理机制),还吸纳了受 Tony Hoare 提出的 CSP(通信顺序进程)启发的语言(例如 Newsqueak 和 Limbo)中的并发相关理念。不过,Go 归根结底是一门全新的编程语言。

一个新创建的 goroutine 初始只会分配几 KB 的栈空间 —— 这几乎能满足绝大多数场景的需求。当栈空间不足时,运行时会自动为栈内存扩容(也可缩容),这使得大量 goroutine 仅需占用少量内存就能同时存在。该机制带来的 CPU 开销平均下来,每个函数调用仅需增加约三条低成本指令。在同一个地址空间内创建数十万个 goroutine 是完全可行的;而如果 goroutine 仅仅是系统线程的话,系统资源在数量远低于此的情况下就会耗尽。

Go 是面向对象语言吗? 可以说是,也可以说不是。尽管 Go 拥有类型和方法,也支持面向对象风格的编程,但它没有类型层级(继承体系)。Go 中的 “接口(interface)” 概念提供了一种截然不同的实现思路 —— 我们认为这种方式更易用,且在某些方面具备更强的通用性。此外,Go 也支持将一个类型嵌入到其他类型中,以此实现类似(但并非完全等同)子类化的效果。由于不存在类型层级,Go 中的 “对象” 相比 C++、Java 等语言显得轻量化得多。

如何实现方法的动态分派? 唯一方式是通过接口。

为什么不支持类型继承? 至少在主流的面向对象语言中,面向对象编程往往过度纠结于类型之间的关系 —— 而这些关系本可以自动推导得出。Go 选择了另一种思路:它不要求程序员提前声明两个类型之间的关联,而是只要一个类型实现了某个接口的全部方法子集,就自动满足该接口。一个类型可以同时满足多个接口,且无需面对传统多重继承的复杂性。这种隐式的类型依赖风格虽然需要一点时间适应,但却是 Go 最能提升开发效率的特性之一。

为什么 Go 不支持方法和运算符重载? 如果方法分派无需同时进行类型匹配,整个逻辑会大幅简化。从其他语言的实践经验来看,同名但签名不同的方法偶尔有用,但在实际开发中也容易造成混淆和脆弱性(比如签名变更导致隐式错误)。Go 的类型系统做了一个关键的简化决策:仅通过名称匹配方法,并要求类型保持一致。 至于运算符重载,它更多是一种便捷性特性,而非硬性需求。同样,去掉它能让语言逻辑更简单。

为什么 Go 没有 “implements” 声明? 在 Go 中,一个类型只需实现某个接口的所有方法,就自动 “实现” 了该接口 —— 无需额外声明(,可类比动态语言中的 )。

我能将 [] T 转换为 [] interface {} 吗? 不能直接转换。这是 Go 语言规范明确禁止的,因为这两种类型在内存中的表示形式并不相同。必须将切片中的元素逐个复制到目标切片中。

若 T1 和 T2 具有相同的底层类型,我能将 [] T1 转换为 [] T2 吗? var t1 T1; var x = T2(t1) // OK,但var st1 []T1; var sx = ([]T2)(st1) // NOT OK。在 Go 语言中,类型与方法紧密关联,因为每个命名类型都拥有一个(可能为空的)方法集。一般规则是:你可以将值转换为另一种命名类型(从而可能改变其方法集),但无法更改复合类型中元素的类型(及其方法集)。Go 要求所有类型转换都必须显式进行。

为什么 Go 不提供隐式数值转换? C 语言中数值类型之间自动转换带来的便利,远抵不上它造成的混乱。自动转换也会让编译器变得更复杂;C 语言的 “寻常算术转换” 并不容易实现,且在不同架构下行为不一致。

Go 中的常量是如何工作的?233.14159math.Pi 这样的字面常量,存在于一种 “理想数值空间” 中,拥有任意精度,不会上溢或下溢。只有当常量或常量表达式被赋值给变量(程序中的内存位置)时,它才会变成一个具有常规浮点数属性和精度的 “计算机数值”。因为常量只是数字,而不是带类型的值,它们比变量使用更自由,从而缓解了严格转换规则带来的一些不便,例如math.Sqrt(2)

Go 有编程风格指南吗? Go 没有明确的官方风格指南,但确实存在一套公认的「Go 风格」。Go 已经形成了关于命名、代码排版和文件组织的约定。《Effective Go》文档中包含了这方面的一些建议。更直接的是,gofmt 程序是一个代码格式化工具,其目的是强制统一代码排版规则。

函数参数什么时候是按值传递的? 和 C 语言家族中的所有语言一样,Go 中一切都是按值传递的。也就是说,函数永远得到被传递值的一份副本。

什么时候应该使用指向接口的指针? 几乎永远不要用。一个常见错误是:把指向接口值的指针传给一个期望接收接口的函数。虽然具体类型的指针可以实现接口,但除一种特殊情况外,指向接口的指针永远不能实现接口。唯一的例外是:任何值,甚至是指向接口的指针,都可以赋值给空接口类型(interface{})变量。即便如此,使用指向接口的指针几乎一定是错误的,结果会非常令人困惑。

我应该在值上还是指针上定义方法? 定义类型的方法时,接收器的行为完全等同于方法的参数。所以,接收器用值还是指针,和「函数参数应该用值还是指针」是同一个问题。方法是否需要修改接收器?如果需要修改,接收器必须是指针。如果接收器很大(比如很大的结构体),用指针接收器成本更低。对于基本类型、slice、小型结构体这类类型,值接收器开销很小。如果某个类型的某些方法必须用指针接收器,那么其余方法也最好用指针接收器,保证无论怎么使用该类型,方法集合都是一致的。

newmake 有什么区别? new 只分配内存;make 用于初始化 slice、map、channel 这三种类型。

64 位机器上 int 的大小是多少? int 和 uint 的大小由具体实现决定,但在同一平台上二者大小相同。为了可移植性,依赖特定大小的代码应该使用显式指定大小的类型,如 int64。另一方面,浮点数和复数类型永远是固定大小的(没有 float 或 complex 这种不确定大小的基本类型)。无类型浮点常量的默认类型是 float64。如果要声明 float32 变量并用无类型常量初始化,必须显式指定类型。

如何知道变量是分配在堆上还是栈上? 从正确性角度看:你不需要知道。Go 中的每个变量,只要还有引用存在,就会一直存在。尽可能情况下,Go 编译器会把函数局部变量分配在该函数的栈帧里;但如果编译器无法证明变量在函数返回后不会被引用,就必须把变量分配在支持垃圾回收的堆上,以避免悬空指针错误;另外,如果局部变量非常大,放在堆上可能比栈上更合理。

为什么我的 Go 进程占用这么多虚拟内存? Go 内存分配器会预留一大块虚拟内存作为分配内存的 “内存池(arena)”。

哪些操作是原子操作?互斥锁(mutex)呢? Go 中操作的原子性相关说明可以在《Go 内存模型》文档中找到。底层同步原语与原子操作可以通过 sync 和 sync/atomic 包使用。这些包适用于简单任务,例如递增引用计数或保证小范围的互斥访问。对于更高级的操作(比如并发服务之间的协调),使用更高级的并发设计可以写出更优雅的程序,而 Go 通过 goroutine 和 channel 支持这种方式。

如何控制使用的 CPU 数量? 可以同时执行 goroutine 的 CPU 数量由 shell 环境变量 GOMAXPROCS 控制,其默认值为可用的 CPU 核心数。将其设为 1 会消除真正的并行能力,强制各个独立的 goroutine 轮流执行。

为什么没有 goroutine ID? Goroutine 没有名称,它们只是匿名的执行单元。它们不会向开发者暴露唯一标识符、名称或数据结构。如果线程或 goroutine 带有名称或 ID,由此形成的使用模式会限制使用它们的库的能力。一旦给某个 goroutine 命名并围绕它构建模型,它就变成了 “特殊” 的,人们会倾向于把所有计算都绑定到这个 goroutine,而忽略使用多个、可能共享的 goroutine 来处理任务。

为什么 T*T 拥有不同的方法集? 正如 Go 语言规范所述:类型 T 的方法集包含所有以 T 为接收器类型的方法;对应的指针类型 *T 的方法集,则包含所有以 *TT 为接收器类型的方法。这意味着*T 的方法集包含了 T 的方法集,但反之不成立。这种区别的根源在于:如果接口值中存储的是指针 *T,方法调用可以通过解引用指针获取对应的值;但如果接口值中存储的是值 T,方法调用没有安全的方式获取其指针。

为什么 Go 没有 ? : 运算符? 语言设计者发现,这个运算符常被用来编写极其复杂、难以理解的表达式。虽然 if-else 写法更长,但语义绝对清晰。一门语言只需要一种条件控制流结构就足够了

Go 的泛型和其他语言的泛型相比如何? 所有语言的基础功能都相似:都可以用 “后续再指定” 的类型来编写类型和函数。尽管如此,仍存在一些区别。

  • Java 编译器在编译期检查泛型类型,但在运行时会把类型擦除,这叫作类型擦除。例如编译期的 List<Integer>,到运行时就变成了非泛型的 List。这意味着在使用 Java 反射时,无法区分 List<Integer>List<Float>。而 Go 里泛型类型的反射信息会包含完整的编译期类型信息。Java 使用类型通配符(如List<? extends Number>List<? super Number>)实现泛型的协变与逆变。Go 没有这些概念,这让 Go 的泛型类型简单很多。
  • 传统 C++ 模板不对类型实参做任何约束,尽管 C++20 通过 concept 支持可选约束。而 Go 中所有类型参数都必须带约束,Go 的约束是接口类型,定义所有允许的类型实参集合。
  • Rust 中的约束叫作 trait bound,trait bound 与类型之间的关联必须显式定义。在 Go 中,类型实参隐式满足约束,就像 Go 类型隐式实现接口一样。Rust 标准库为比较、加法等操作定义了标准 trait;Go 标准库没有,因为这些可以在用户代码中通过接口类型表达。唯一例外是 Go 预定义的 comparable 接口,它表达了类型系统本身无法表达的属性。
  • Python 不是静态类型语言,因此可以合理地说:所有 Python 函数默认都是泛型的 —— 总能用任意类型的值调用,任何类型错误都在运行时检测。

为什么 Go 不支持带类型参数的方法? Go 允许泛型类型拥有方法,但除接收器外,这些方法的参数不能使用参数化类型。预计 Go 未来也不会添加泛型方法,问题在于如何实现它们。Go 极大受益于纯提前编译的简单性和性能可预测性,我们不愿只为一个语言特性引入 JIT(在运行时编译需要的方法代码) 复杂度等。替代方案:使用带类型参数的顶层函数;或者把类型参数加到接收器类型上。

如何编写单元测试? 在包源码所在的同一目录下,新建一个以 _test.go 结尾的文件。在该文件内,导入 testing 包,并编写如下格式的函数:func TestFoo(t *testing.T) {}。在该目录下运行 go test。该命令会找到所有 Test 开头的函数,构建测试二进制文件并运行。

为什么没有指针运算? 为了安全。没有指针运算,就可以设计出一门永远不会非法构造出错误地址的语言。此外,去掉指针运算也能简化垃圾回收器的实现。

为什么 ++-- 是语句而不是表达式? 没有指针运算后,自增、自减运算符作为前缀/后缀的价值大幅下降。把它们完全移出表达式体系,可以简化表达式语法,同时消除求值顺序带来的各种混乱问题。

为什么左大括号不能另起一行? Go 借鉴了 BCPL 的技巧:分隔语句的分号在正式语法中存在,但词法分析器会在语句可能结束的行尾自动插入,这在实践中工作得很好,但也强制了一种大括号风格。全 Go 程序统一、强制的格式化带来的收益,远大于某种具体风格的缺点。

为什么要使用垃圾回收? GC 不会开销太大吗?系统程序里最麻烦的工作量之一,就是管理分配对象的生命周期。在 C 这类手动管理的语言中,这会消耗程序员大量精力,也常常是各种隐蔽 Bug 的根源。即使在 C++、Rust 这类提供辅助机制的语言中,这些机制也会显著影响软件设计,带来自身的编码负担。我们认为消除这类程序员负担至关重要。而近几年垃圾回收技术的进步,让我们确信:GC 可以实现得足够低成本、足够低延迟,能够用于网络服务系统。自动垃圾回收让并发代码容易写得多。Go 选择了更传统的路线:只用垃圾回收来解决对象生命周期问题。当前 Go 的 GC 实现是标记 — 清除回收器。在多核机器上,回收器会在单独的 CPU 核心上与主程序并行运行。

语法简介

语法PythonGo
变量声明动态类型,无需声明静态类型,必须声明
类型转换int(), str(), float(), tuple(), list(), set(), dict()type_name(exp), strconv.Atoi(), strconv.Itoa(), strconv.ParseFloat(), string(), value.(type), value.(T)
输入输出print(), input()fmt.Println(), fmt.Scan()
代码块缩进(空格/制表符){} 包裹
循环for、while只有 for
函数def,支持默认参数func,无默认参数
错误处理try-except返回 error + if err != nil
并发threading(GIL 限制)goroutine + channel
面向对象完整类继承struct + interface
包管理pip + importgo mod + import

内置函数

Python 的内置函数更丰富,适合快速开发;Go 的内置函数较少,但更专注于底层控制和性能优化。

Go

  1. 内存分配:new(), make()
  2. 类型转换:int(), float64(), string()
  3. 集合操作:len(), cap(), append()
  4. 并发:go, chan, close()
  5. 错误处理:panic(), recover()

Python

  1. 输入输出:print(), input()
  2. 类型转换:int(), float(), str(), bool()
  3. 数学运算:abs(), round(), min(), max(), sum()
  4. 迭代与序列操作:len(), range(), sorted(), enumerate()
  5. 对象操作:type(), isinstance(), dir()
  6. 高阶函数:map(), filter(), zip()

控制流

循环

go
/* Go 只有 for 循环 */

for i, val := range arr {
  // ...
}

for i := 0; i < len(arr); i++ {
  // ...
}
python
for i in range(10):
  pass

for i in range(0, 10, 1):
  pass

for val in arr:
  pass

for i, val in enumerate(arr):
  pass
else
  pass

while len(queue) > 0:
  pass
else
  pass

while flag: print(str(flag))

数据结构

go
stack := make([]string, 0, 10)

// 入栈
stack = append(stack, "A")

// 出栈
v := stack[len(stack)-1]
stack = stack[:len(stack)-1]

// 判断是否为空
isEmpty := len(stack) == 0
python
L = []

L.append('D')  # 入栈

L.pop()  # 出栈
L[-1]  # peek

队列

go
queue := make([]int, 0)

// 入队
queue = append(queue, 10)

// 出队
v := queue[0]
queue = queue[1:]

// 判断是否为空
isEmpty := len(queue) == 0
python
L = []

L.append('D')  # 入队

L.pop(0)  # 出队
L[0]  # peek
  • collections.deque 是个高效的双端队列,具有 popleft() append() 方法。

集合

集合(set)是一个无序的不重复元素序列。

如果从广义上来看的话,数组、字典、元组、栈、队列等都是集合。

Go 无原生 Set,通常用 map[T]boolmap[T]struct{} 模拟。一般还会封装成一个结构体。

go
// 初始化一个 set(用 map[string]bool 模拟)
set := make(map[string]bool)

// 添加元素
set["apple"] = true
set["banana"] = true

// 检查元素是否存在
if set["apple"] {
    fmt.Println("apple exists") // 输出: apple exists
}

if _, ok := set["apple"]; ok {
  // 当使用 map[string]struct{} 时更省内存(struct{} 是零大小类型)
    fmt.Println("apple exists") // 输出: apple exists
}

// 删除元素
delete(set, "banana")

// 遍历集合
for key := range set {
    fmt.Println(key) // 输出: apple
}
python
set1 = {1, 2, 3, 4}            # 直接使用大括号创建集合; 创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典
set2 = set([4, 5, 6, 7])      # 使用 set() 函数从列表创建集合

set2.add(8)  # 将元素 x 添加到集合 s 中,如果元素已存在,则不进行任何操作
set2.update([9,10])  # 参数可以是列表,元组,字典等

set2.remove(8)  # 将元素 x 从集合 s 中移除,如果元素不存在,则会发生错误
set2.discard(8)  # 移除集合中的元素,且如果元素不存在,不会发生错误
set2.pop()  # 随机删除集合中的一个元素
set2.clear()  # 清空集合

x in s  # 判断元素 x 是否在集合 s 中,存在返回 True,不存在返回 False
len(s)  # 计算集合 s 元素个数

字典

go
// 字典是引用类型,必须初始化后再使用,键必须是可比较的类型
var myMap map[string]int  // 此时为 nil
// myMap["key"] = 1  // 如果没有初始化会 panic
myMap = make(map[string]int)  // 使用 make,会根据存储的键值对数量动态调整其容量,也可指定一个初始容量,有助于减少容器扩容带来的内存分配和重新哈希次数
myMap = map[string]int{"one": 1}  // 字面量

// 添加/修改:
m["key"] = value

// 读取,键不存在返回value类型的零值
value := m["key"]

// 删除,键不存在则静默处
delete(m, "key")

// 检查键是否存在
if value, exists := myMap["key"]; exists {
    // 处理 value
}

// 遍历
for key, value := range myMap {
    // ...
}

// 并发安全方法 1:sync.RWMutex 读写操作都比较均衡
var m = make(map[string]int)
var mutex = &sync.RWMutex{}

// 写操作加锁
mutex.Lock()
m["key"] = 1
mutex.Unlock()

// 读操作加读锁
mutex.RLock()
value := m["key"]
mutex.RUnlock()


// 并发安全方法 2:sync.Map 读多写少(例如缓存)
// Store(存储)、Load(加载)、Delete(删除)、Range(遍历)
var m sync.Map  // 开箱即用,无需初始化(因为 sync.Map 的零值即未初始化的状态就是可用的,声明时在内部已经处理了初始化的逻辑,在 sync.Map 的内部实现中,如果底层的存储结构尚未初始化,会在第一次使用时进行初始化,这也是 Go 的一个设计哲学:类型的零值应该是立即可用的),其键和值类型都是interface{}
    
// 存储键值对
m.Store("name", "Alice")
m.Store("age", 25)
m.Store("city", "Beijing")

// 加载值
if value, ok := m.Load("name"); ok {
    fmt.Println("Name:", value) // Name: Alice
}

// 删除键
m.Delete("city")

// 检查键是否存在
if _, ok := m.Load("city"); !ok {
    fmt.Println("City key deleted")
}
python
# 空字典,字典可以动态增长和收缩
empty_dict = {}
empty_dict2 = dict()

# 带初始值的字典,键必须是可哈希的(不可变类型),值可以是任意类型
person = {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
person2 = dict(name='Bob', age=30, city='Shanghai')  # 关键字参数形式

# 从键值对序列创建
items = [('name', 'Charlie'), ('age', 35), ('city', 'Guangzhou')]
person3 = dict(items)

# 字典推导式
squares = {x: x*x for x in range(1, 6)}  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# 访问元素
print(person['name'])           # Alice
print(person.get('age'))        # 25

# 安全的访问方式
print(person.get('country'))    # None (不会报错,避免 KeyError)
print(person.get('country', 'China'))  # China (提供默认值)

# 修改和添加
person['age'] = 26              # 修改已有键
person['country'] = 'China'     # 添加新键

# 删除元素
del person['city']              # 删除键值对
age = person.pop('age')         # 删除并返回值
key, value = person.popitem()   # 删除并返回最后插入的项

# 清空字典
person.clear()

# 获取所有键
keys = person.keys()            # dict_keys(['name', 'age', 'city'])
print(list(keys))               # ['name', 'age', 'city']

# 获取所有值
values = person.values()        # dict_values(['Alice', 25, 'Beijing'])

# 获取所有键值对
items = person.items()          # dict_items([('name', 'Alice'), ('age', 25), ('city', 'Beijing')])

# 检查键是否存在
print('name' in person)         # True

# 字典长度
print(len(person))              # 3

# 合并字典方法 1:update 
person.update({'age': 26, 'city': 'Beijing'})
print(person)   # {'name': 'Alice', 'age': 26, 'country': 'China', 'city': 'Beijing'}

# 合并字典方法 2:解包 (Python 3.5+)
dict1 = {'a': 1, 'b': 2}
merged = {**dict1, **dict2}
print(merged)  # {'a': 1, 'b': 3, 'c': 4}

# 合并字典方法 3: | 运算符 (Python 3.9+)
merged = dict1 | dict2
print(merged)  # {'a': 1, 'b': 3, 'c': 4}

# 浅拷贝
original = {'a': 1, 'b': [2, 3]}
shallow_copy = original.copy()

# 深拷贝
import copy
deep_copy = copy.deepcopy(original)
deep_copy['b'].append(5)
print(original)  # {'a': 1, 'b': [2, 3, 4]} - 原字典不受影响

# 遍历键
for key in person:
    print(key)

for key in person.keys():
    print(key)

# 遍历值
for value in person.values():
    print(value)

# 遍历键值对
for key, value in person.items():
    print(f"{key}: {value}")

# 带索引的遍历
for i, (key, value) in enumerate(person.items()):
    print(f"{i}: {key} = {value}")

# 按键排序
sorted_by_key = dict(sorted(scores.items()))

# 按值排序
sorted_by_value = dict(sorted(scores.items(), key=lambda x: x[1]))

# 按值降序
sorted_desc = dict(sorted(scores.items(), key=lambda x: x[1], reverse=True))

# 在 asyncio 协程中,因为所有任务在单线程内通过事件循环调度,不存在真正的并行执行,所以在协程中操作字典是安全的,无需额外加锁
# 多线程安全需要上锁
# 多进程之间不共享内存,也无需考虑
import threading

my_lock = threading.Lock()

# 非线程安全的复合操作
if "key" not in my_dict:
    my_dict["key"] = value  # 在检查后、设置前,其他线程可能已经修改了字典

# 线程安全的复合操作
with my_lock:  # 假设 my_lock 是已经定义好的锁
    if "key" not in my_dict:
        my_dict["key"] = value
python
from collections import defaultdict, OrderedDict, Counter

# 默认字典,有默认值的字典
word_count = defaultdict(int)
word_count["hello"] += 1  # 自动初始化为0

# 计数器
counter = Counter("hello world")
print(counter)  # Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})

# 有序字典(现在普通字典也有序了)
ordered = OrderedDict([('a', 1), ('b', 2), ('c', 3)])

字符串

串是特殊的线性表,字符串的操作一般有:

  • 字符串替换
  • 字符串查找
  • 字符串连接
  • 字符串分割
  • 大小写转换
  • 去除空格
  • 字符串判断
  • 字符串格式化
  • 字符串构建

两种语言的字符串都是不可变的。需要注意的是:

  • Python3 的字符串是 Unicode 码点序列,Go 字符串是 UTF-8 字节序列(rune 才是 Unicode 码点)
  • Python 使用对象方法,而 Go 使用 strings 包函数
  • Go 需要显式错误处理,Python 使用异常
  • 索引访问时,Python 直接返回字符,Go 返回字节
  • Python支持负数索引,Go 不支持
  • Python没有单独的字符类型,字符串(str)是由单个字符组成的序列,也就是说,一个字符就是一个长度为1的字符串;而在 Go 中处理 ASCII 字符时常用 byte,而处理 Unicode 字符时常用 rune,按字符遍历,用 for range 循环时会将字符串解码为 rune。
go
// 字符串替换
s := "hello world"
newS := strings.Replace(s, "world", "golang", -1) // "hello golang"
newS2 := strings.Replace(s, "l", "L", 2)          // "heLLo world"

// 字符串判断
s := "hello"
starts := strings.HasPrefix(s, "he")  // true
ends := strings.HasSuffix(s, "lo")    // true
// Go标准库没有直接的isalpha/isdigit,需要自定义或使用unicode包
isAlpha := true
for _, r := range s {
    if !unicode.IsLetter(r) {
        isAlpha = false
        break
    }
}

// 字符串分割
strings.Split("a,b,c", ",") // ["a" "b" "c"], Split slices s into all substrings separated by sep and returns a slice of the substrings between those separators.
strings.Split("a man a plan a canal panama", "a ") // "" "man " "plan " "canal panama"]
strings.Split(" xyz ", "")  // [" " "x" "y" "z" " "], If sep is empty, Split splits after each UTF-8 sequence.
strings.SplitN("a,b,c,d", ",", 3)  // []string{"a", "b", "c,d"}

// 字符串转整数
s := "123"
numInt, err := strconv.Atoi(s)           // 123
if err != nil {
    fmt.Println("转换错误:", err)
}
// 指定基数的转换
numHex, _ := strconv.ParseInt("ff", 16, 64)  // 255 - 十六进制
numBin, _ := strconv.ParseInt("1010", 2, 64) // 10 - 二进制
// 字符串转浮点数
sFloat := "3.14"
numFloat, err := strconv.ParseFloat(sFloat, 64) // 3.14
if err != nil {
    fmt.Println("转换错误:", err)
}
// 无符号整数转换
numUint, _ := strconv.ParseUint("123", 10, 64)
// 布尔值转换
boolVal, _ := strconv.ParseBool("true") // true
fmt.Println(numInt, numHex, numBin, numFloat, numUint, boolVal)
// 安全转换函数
func safeAtoi(s string) int {
    if num, err := strconv.Atoi(s); err == nil {
        return num
    }
    return 0
}
result := safeAtoi("123")  // 123
result2 := safeAtoi("abc") // 0
python
# 字符串替换
s = "hello world"
new_s = s.replace("world", "golang")  # "hello golang"
new_s2 = s.replace("l", "L", 2)       # "heLLo world"

# 字符串判断,Python 中蛮多方法的
s = "hello"
starts = s.startswith("he")   # True
ends = s.endswith("lo")       # True
is_alpha = s.isalpha()        # True, Return True if all characters in the string are alphabetic and there is at least one character, False otherwise.
is_digit = s.isdigit()        # False, Return True if all characters in the string are digits and there is at least one character, False otherwise. # isdecimal() ⊆ isdigit() ⊆ isnumeric()

# 字符串分割
'1,2,3'.split(',')  # ['1', '2', '3'], Return a list of the words in the string, using sep as the delimiter string.
'1,2,3'.split(',', maxsplit=1)  # ['1', '2,3']
'1,2,,3,'.split(',')  # ['1', '2', '', '3', '']
'1 2 3'.split()  # ['1', '2', '3'] If sep is not specified or is None, a different splitting algorithm is applied: runs of consecutive whitespace are regarded as a single separator, and the result will contain no empty strings at the start or end if the string has leading or trailing whitespace.
'   1   2   3   '.split()  # ['1', '2', '3']

# 字符串转整数
s = "123"
num_int = int(s)           # 123
num_hex = int("ff", 16)    # 255 - 十六进制
num_bin = int("1010", 2)   # 10 - 二进制

# 字符串转浮点数
s_float = "3.14"
num_float = float(s_float) # 3.14
# 错误处理
try:
    num = int("abc")
except ValueError as e:
    print(f"转换错误: {e}")
# 安全转换(不会抛出异常)
def safe_int(s, default=0):
    try:
        return int(s)
    except ValueError:
        return default

result = safe_int("123")  # 123
result2 = safe_int("abc") # 0

语言特性

类型

  • 在 Python 中,空容器(如空列表、空字符串、空字典等)在布尔上下文中被视为 False,而非空容器被视为 True,所以可以使用 while stack 表示栈不为空时的循环。
  • 圆括号 () → 元组,方括号 [] → 列表,花括号 {} → 集合或字典

函数

  • 在 Java 中,如果在函数中修改传递给函数的引用所对应的对象,是值传递,但是传递的是引用的值,会影响原对象。通过引用操作对象,效率高但需要注意副作用。

  • 在 Go 中,如果在函数中修改传递给函数的 slice 或 map 里的元素,也是值传递,但由于这个值包含指向底层数组的指针,实际上也是传递的引用,会影响原切片,但使用 append() 可能不会影响原切片。Go 更明确地控制内存分配和修改行为。

  • 在 Python 中,如果在函数中修改传递给函数的可变对象(如列表),实际上传递的是对象的引用,跟 Java 类似,会直接影响原对象。通过引用操作对象,效率高但需要注意副作用。

计算

  • Java 中, / 的行为取决于操作数类型,整数 / 整数 → 整数除法(截断),浮点数 / 整数 → 浮点除法,在设计上更注重类型安全和性能。

  • Go 中, / 的行为取决于操作数类型,整数 / 整数 → 整数除法(截断),浮点数 / 浮点数 → 浮点除法,浮点数 / float64(整数)→ 浮点除法,在设计上更注重类型安全和性能,同时“显式优于隐式”,Go 的类型系统更严格,相比 Java 没做整数到浮点数的自动类型提升。

  • Python 中的除法运算符 / 总是返回一个浮点数(即使两个操作数都是整数并且可以整除),如果需要执行整数除法(即向下取整),可以使用 // 运算符。Python 的设计哲学强调代码的清晰性和可读性,“让简单的事情简单,复杂的事情可能” ,/ 总是进行真除法(true division),返回数学上准确的结果。

相关文章
2023-02-24 55分钟 11067 字

动机 自己作为独立开发者,也想体验那种写完代码效果就出来的感觉,不用又当个运维人员。 想当初 Spring Boot 程序,得手动编译,然后手动复制到目标机器 …

阅读更多 →
2025-05-09 5分钟 1041 字

Magic Python -> ch4 heapq 是 Python 的一个标准模块,它提供了堆排序算法的实现。 带 yield 的函数是一个生成器,而 …

阅读更多 →
2023-07-08 3分钟 764 字

动机 想要手头的两台 MBP 能同时无延迟播放音频,拒绝使用直播流的形式。 Tip update at 2025-04-13

阅读更多 →