前言

笔者此前主要使用 Python 进行数据分析与脚本编写,近期出于学习需要开始接触 Go 语言。Python 的动态类型与极简语法很友好,而 Go 是编译型、静态类型的语言,二者在”怎么声明变量、怎么组织代码”上的思路差异很大。本笔记以 Python 为参照,把 Go 的核心语法逐条对照整理,方便像笔者一样从 Python 转过来的同学快速建立对应关系。

总体印象

维度PythonGo
执行方式解释执行(python xx.py编译执行(go run / go build 出 exe)
类型系统动态类型,运行期推断静态类型,编译期检查
代码块缩进花括号 {}
语句结尾不需要分号编译器自动加分号,但一行一语句
程序入口if __name__ == '__main__'func main()(唯一入口)
未使用变量不报错编译报错
未使用 import不报错编译报错

一句话总结:Python 宽松、Go 严格。Go 把很多”你自己负责”的事(类型、变量是否使用、错误处理)在编译期就强制检查。

一、程序入口

1
2
3
4
5
# Python
def main():
print("Hello")
if __name__ == '__main__':
main()
1
2
3
4
5
6
7
8
// Go
package main

import "fmt"

func main() {
fmt.Println("Hello")
}

区别在于:Python 任何文件都可以”顶层直接跑代码”,而 Go 程序只能从 main 包下的 main() 函数开始执行。其它函数(比如 main1main2)即使定义好了,不被调用就永远不会执行——这是 Go 新手最容易迷惑的一点。

二、导入 import

1
2
3
4
# Python
import os
from os import path
import numpy as np
1
2
3
4
5
6
7
8
9
10
11
// Go 单个
import "fmt"

// Go 多个
import (
"fmt"
"os"
)

// Go 第三方包,路径要写全,代码里用最后一段作包名
import "github.com/gin-gonic/gin" // 代码里用 gin

关键差异:

  • Python 的 from xx import yy 可以直接用 yy;Go 没有这种写法,一律 包名.函数名 调用。
  • Go 里 import 了却没用到 → 编译报错 imported and not used: fmt。所以代码里删掉某功能时,记得同步删 import。
  • 对应关系:Python 的 import os ≈ Go 的 import "os"numpypandas 这类第三方库,Go 里是 github.com/xxx/yyy 这样的完整路径。

三、变量声明

Python 里 x = 5 就行,声明这件事被隐藏了。Go 则必须显式声明,且有多种写法

1
2
3
4
5
var age int = 15        // ① 完整声明:var + 名字 + 类型 + 值
var score float64 // ② 只声明类型,不赋值 → 得到"零值"0
score = 0.941 // 之后单独赋值
name := "henny" // ③ 短声明 :=,类型由编译器推断,只能在函数体内用
var a = "RUNOOB" // ④ var 但省略类型,交给编译器推断(同 := 效果,但顶层也能用)

与 Python 的对应:

PythonGo备注
x = 5x := 5(函数内)或 var x = 5Go 必须写明是声明
无此概念var x int只声明会得到零值,不是报错
无此概念x := 5简写,只能在函数体内

新手最容易踩的两个坑(笔者都踩了):

1
2
3
4
5
6
7
8
9
10
// 坑1:声明了但没使用
func main() {
b := true // 声明了
// 但没有"读"过它 → 编译报错: declared and not used: b
}
// 注意:只"赋值"不算使用,必须被"读取"(打印、判断、运算)才算

// 坑2::= 写到了函数外面
number_new := [6]int{1, 2, 3, 4, 5, 6} // 顶层不允许语句 → 语法错误
// 顶层只能写"声明"(var/const/type/func),凡是"做事"的代码都必须放进函数里

四、数据类型

类型PythonGoGo 零值
整数intint0
小数floatfloat640
字符串strstring""(空串)
布尔bool(True/False)bool(true/false,小写false
空值Nonenil(指针/切片/字典等用)取决于类型

差异点:

  • Go 的布尔值是 小写 true / false,Python 是大写 True / False
  • Python 变量类型可变(x = 5x = "a" 合法);Go 变量类型一旦定下不可变,给 int 变量赋字符串直接编译报错。
  • Go 声明变量不赋值时会自动填零值,而 Python 变量必须先赋值才能用(否则 NameError)。

五、常量

1
2
# Python:惯例用大写,但本质还是变量,随时能改
PI = 3.14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Go:真正的常量,编译期不可修改
const Pi = 3.14
const Appname = "NOOB"

// 常量组 + iota(常量计数器)
const (
a = iota // 0,iota 从 0 开始,每行自动 +1
b // 1
c // 2
d = "ha" // 独立值,iota 仍 +1
e // "ha",沿用上一行表达式
f = 100 // 独立值
g // 100
h = iota // 7,恢复使用 iota
i // 8
)
  • Python 的”常量”只是命名约定;Go 的 const 是语法层面的不可变。
  • iota 是 Go 独有的计数器:在 const 块里每新起一行自动 +1,适合定义枚举、周几、状态码这类递增序列。

六、运算符

三者(Python / Go)基本一致,写法对照:

分类PythonGo
算术+ - * / %同左;整除 Python 用 //,Go 里 int/int 天然整除
幂运算**没有 **,用 math.Pow(x, y)
自增/减x += 1(无 ++x++ / x--(是语句,不是表达式)
比较== != < > <= >=同左
逻辑and or not&& || !(三个符号形式)
赋值复合+= -= *= /=同左,另有 <<= >>= &= ^= |=(位运算)

重点:Python 的 and/or/not 变成了 Go 的 &&/||/!,这是写条件时最容易手滑的地方。

七、条件判断

1
2
3
4
5
6
7
# Python
if x > 0:
print("正数")
elif x == 0:
print("零")
else:
print("负数")
1
2
3
4
5
6
7
8
// Go
if x > 0 {
fmt.Println("正数")
} else if x == 0 {
fmt.Println("零")
} else {
fmt.Println("负数")
}

差异:

  • Python 用缩进区分分支,Go 用 {}else if} 必须在同一行,换行会报错。
  • Go 的判断条件不需要括号,直接 if x > 0
  • Go 支持在 if 里先写初始化语句(Python 没有):
1
2
3
if score := getScore(); score >= 60 {  // 先声明 score 再判断
fmt.Println("及格", score)
} // score 只在 if 块内有效

八、循环

Go 只有 for 一种循环关键字,用不同写法覆盖 Python 的 forwhilefor...else 等场景:

PythonGo
for i in range(10)for i := 0; i < 10; i++
for x in arrfor i, x := range arr
while cond:for cond {}
while True:(死循环)for {}
break / continuebreak / continue(一致)
1
2
3
# Python
for i in range(5):
print(i)
1
2
3
4
5
6
7
8
9
10
11
12
// Go
for i := 0; i < 5; i++ {
fmt.Println(i)
}

// 遍历切片/数组/字典:range
for index, value := range names {
fmt.Println(index, value) // 只想要 value 就用 _ 占位 index
}
for _, value := range names { // 忽略下标
fmt.Println(value)
}

提醒:Go 里 for {} 是死循环,和 Python 的 while True: 一样,若没有 break 程序会一直跑(笔者文件里就留着一个 main6 死循环,切记别从 main 调它)。

九、函数

1
2
3
# Python
def add(a, b):
return a + b
1
2
3
4
// Go
func add(a int, b int) int {
return a + b
}

语法结构:func 函数名(参数列表) 返回值类型 { ... }。参数多个同类型可简写:func add(a, b int) int

Go 最大的特色是支持多返回值(Python 只能返回一个值,多个要靠元组):

1
2
3
4
5
6
7
8
func swap(x, y string) (string, string) {  // 第一个括号收参数,第二个括号给返回值
return y, x
}

func main() {
a, b := swap("Google", "Runoob") // 按顺序接住两个返回值
fmt.Println(a, b)
}

这个特性最大的实际用途是错误处理——Go 约定函数返回 (结果, error),调用方要检查错误:

1
2
3
4
5
6
f, err := os.Open("a.txt")
if err != nil { // 错误不为空说明出错了
fmt.Println("打开失败:", err)
return
}
defer f.Close() // 函数结束时关闭文件(类似 Python 的 with)

对比 Python 的 try/except:Python 用异常抛出错误,Go 用返回值显式传递错误。这是两种哲学:Python 信任调用方会处理异常,Go 强制调用方每次都要面对错误

十、数组、切片与列表

PythonGo说明
list(可变长)切片 []int{1,2,3}动态长度,最常用
数组 [6]int{1,2,3,4,5,6}定长,长度是类型的一部分
dictmap map[string]int{"a":1}字典
1
2
3
4
5
6
7
8
9
10
arr := [6]int{1, 2, 3, 4, 5, 6}   // 数组:定长6,不能变
sli := []int{1, 2, 3} // 切片:可动态增长
m := map[string]int{"a": 1, "b": 2} // 字典

// 切片追加元素(对应 Python list.append)
sli = append(sli, 4) // [1 2 3 4]

// 索引访问、len 都差不多
fmt.Println(len(sli)) // 4
fmt.Println(arr[0]) // 1

关键坑:Python 里 [1,2,3] 永远可变长,所以直接”赋值”就完事;Go 里数组定长、切片变长,从 Python 转过来时默认应该用切片 []int{},而不是数组 [6]int{}——数组一旦定了长度就不能增删。

十一、map 字典 / struct 结构体 / interface 接口

这是从 Python 转向 Go 时最需要”换脑子”的三块:Python 的 dictclass、鸭子类型,在 Go 里分别是 mapstructinterface。它们不是一一照搬,而是各有侧重。

map:字典

1
2
3
4
5
6
# Python
d = {"a": 1, "b": 2}
d["c"] = 3 # 新增/修改
print(d.get("a")) # 1
print(d.get("x")) # None,键不存在返回 None 不报错
del d["a"] # 删除
1
2
3
4
5
6
7
8
9
10
11
// Go
m := map[string]int{"a": 1, "b": 2}
m["c"] = 3 // 新增/修改

v, ok := m["a"] // 读取:返回 (值, 是否存在)
fmt.Println(v, ok) // 1 true

v2, ok := m["x"] // 键不存在
fmt.Println(v2, ok) // 0 false ← 重点:不存在返回零值,不报错

delete(m, "a") // 删除:是函数,不是方法

map 的核心差异在”取不存在的键”:

  • Python 直接 d["x"] 会抛 KeyError,所以常写成 d.get("x")(不存在返回 None)
  • Go 的 m["x"] 直接返回该类型的零值(数字返回 0),不报错——但这带来一个坑:你分不清”键不存在”和”值本来就是 0”。要区分,必须用两值写法 v, ok := m["x"],看 ok 是 true 还是 false

struct:结构体(对应 class)

Python 里最接近 Go struct 的是 dataclass

1
2
3
4
5
6
7
8
9
10
11
# Python
from dataclasses import dataclass

@dataclass
class User:
name: str
age: int
email: str = "" # 默认值

u = User(name="张三", age=18)
print(u.name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Go
type User struct {
Name string
Age int
Email string
}

func main() {
u := User{Name: "张三", Age: 18} // 按字段名初始化
fmt.Println(u.Name)

u2 := User{"李四", 20, ""} // 按位置初始化(不推荐,字段一变就乱)
fmt.Println(u2.Age)
}

struct 和 class 的最大区别:struct 只有数据字段,没有方法。方法要用”接收者”在类型外面单独定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type User struct {
Name string
Age int
}

// 值接收者:相当于 Python 的 self,但只读不改
func (u User) SayHi() string {
return "Hi, " + u.Name
}

// 指针接收者:才能修改结构体内部
func (u *User) SetAge(age int) {
u.Age = age
}

func main() {
u := User{Name: "张三"}
fmt.Println(u.SayHi()) // Hi, 张三
u.SetAge(18) // 修改成功(指针接收者)
}

还要注意两点:

  • Go 没有继承。Python 的 class B(A) 在 Go 里写不了,替代方案是结构体组合:把一个结构体嵌进另一个里
  • Go 约定:大写开头的字段/函数才对外可见(跨包用),小写是本包私有——这是 Python 的 _ 下划线约定被提升成了语法规则

interface:接口(对应鸭子类型)

Python 的多态靠”鸭子类型”——对象只要有 sound() 方法就能传,运行期才知道对错:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Python:只要会叫就能用,不检查类型
def make_sound(animal):
animal.sound()

class Dog:
def sound(self):
print("汪")
class Cat:
def sound(self):
print("喵")

make_sound(Dog()) # 汪
make_sound(Cat()) # 喵

Go 用接口把这件事显式化,但实现是隐式的(不需要写 implements):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Animal interface {
Sound() string
}

type Dog struct{}
func (d Dog) Sound() string { return "汪" }

type Cat struct{}
func (c Cat) Sound() string { return "喵" }

func MakeSound(a Animal) { // 参数类型是接口
fmt.Println(a.Sound())
}

func main() {
MakeSound(Dog{})
MakeSound(Cat{})
}
  • 接口定义”要有哪些方法”,类型只要方法齐了就自动满足,不需要声明”我实现了 Animal”
  • 好处是:MakeSound 只关心”你会不会 Sound”,不关心你具体是 Dog 还是 Cat——编译期就检查,写错了立刻报错,不会拖到运行期
  • 空接口 interface{}(别名 any)能装任何值,类似 Python 的”什么都能传”。取回来时要类型断言(类似 isinstance):
1
2
3
4
5
6
7
8
9
10
var v any = 42

switch t := v.(type) { // 类型分支,相当于 isinstance 系列
case int:
fmt.Println("整数", t)
case string:
fmt.Println("字符串", t)
default:
fmt.Println("其它", t)
}

三者对照表

概念PythonGo
字典dictmap[K]V
类 / 数据结构class / @dataclassstruct
方法定义def f(self, ...)func (r 类型) F(...)
继承class B(A)无继承,用组合(struct 嵌套)
多态 / 鸭子类型运行期隐式interface,编译期隐式实现
判断类型isinstance(x, T)v, ok := x.(T) 类型断言
判断键存在k in d / d.get(k)v, ok := m[k]

十二、实战对照:训练集划分脚本

笔者博客里有篇《一个简单的训练集划分脚本》,用 Python 把数据集按 0.7 比例划分训练/验证集、预留 100 张测试集。这里用两种语言各写一遍,直观对比风格差异。

Python 版(原脚本核心逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
import os, numpy as np, shutil

imageList = os.listdir('./wormPic/images/')
randomList = [name.replace('.jpg', '') for name in imageList]
np.random.shuffle(randomList)

total = len(randomList) - 100
trainNum = int(0.7 * total)
valNum = total - trainNum

for i in randomList[:trainNum]:
shutil.copy('./wormPic/images/'+i+'.jpg', './out/train/'+i+'.jpg')

Go 版(同逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"io"
"math/rand"
"os"
"strings"
)

func copyFile(src, dst string) { // 复制文件
in, _ := os.Open(src)
defer in.Close()
out, _ := os.Create(dst)
defer out.Close()
io.Copy(out, in)
}

func main() {
entries, _ := os.ReadDir("./wormPic/images/")

var names []string // 切片
for _, e := range entries {
names = append(names, strings.TrimSuffix(e.Name(), ".jpg"))
}

rand.Shuffle(len(names), func(i, j int) { // 打乱
names[i], names[j] = names[j], names[i]
})

total := len(names) - 100
trainNum := int(float64(total) * 0.7)
valNum := total - trainNum
fmt.Println("训练集:", trainNum, "验证集:", valNum, "测试集:", 100)

train := names[:trainNum] // 切片切分,对应 Python 的 [0:trainNum]
val := names[trainNum : trainNum+valNum]
test := names[trainNum+valNum:]

for _, set := range [][]string{train, val, test} {
for _, name := range set {
copyFile("./wormPic/images/"+name+".jpg", "./out/"+name+".jpg")
copyFile("./wormPic/txt/"+name+".txt", "./out/"+name+".txt")
}
}
}

两个语言整体思路一致:列目录 → 打乱 → 切分 → 复制。差异主要在:

  • Python 一行 np.random.shuffle(randomList);Go 要写 rand.Shuffle 加一个交换函数。
  • Python 切片 list[0:n]、Go 切片 slice[0:n] 语法几乎一样。
  • Go 里 err 随处可见(entries, _ := ... 里那个 _ 就是丢弃错误),Python 则没有这个概念。

十三、常见坑速查表(Python 习惯 → Go 报错)

Python 习惯Go 里的结果
x = 5 直接写变量名未声明,报 undefined
变量声明了没用declared and not used
True / False / None应写 true / false / nil,大写报错
顶层直接写 x := 1non-declaration statement outside function body
import numpy 不使唤imported and not used
and / or / not应写 && / || / !
while cond:应写 for cond {}
一个函数返回多个值Go 天生支持,但要用 (type, type) 声明
抛异常处理错误Go 用 返回值 + if err != nil
直接 d["x"] 取不存在的键Python 抛 KeyError;Go 返回零值不报错,区分要写 v, ok := m["x"]
class B(A): 继承Go 没有继承,用 struct 组合
方法里写 selfGo 写接收者 (u User),放在函数名前面
传个 dict 当参数map 或定义 struct;要约束行为就用 interface

结语

从 Python 转向 Go,最难的不是语法本身,而是思维的转变

  1. 从”动态随便写”到”静态严格写”——类型、变量使用、错误处理,Go 都逼你在编译期想清楚。
  2. 从”顶层直接跑”到”一切进函数”——顶层只能声明,逻辑都放函数里,由 main 串联。
  3. 从”异常”到”错误值”——Go 的 err != nil 是日常。

把本笔记的对照表过一遍,再实际动手写两个小程序(比如把上面的训练集脚本移植成 Go),基本上就能完成从 Python 到 Go 的语法官翻。