点开我就当至少你会Python C++ C# Java任意一种了,所以示例代码为主,没有的话不要继续看因为会看不懂。所以安装我就不多说了,Windows去这里下安装包,linux用包管理器。看完本文你将速通Go的基本语法,变量声明,数组,循环,Switch,函数,结构体,Map,错误处理,接口/鸭子类型,指针

先跑起来

创建个新文件夹,然后用vscode打开,搭一下脚手架

1
2
3
4
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
go install golang.org/x/tools/gopls@latest
go mod init go-starter

GO111MODULE?

GO111MODULE=on是开启 Go 的现代包管理模式,并且把这个设置全局保存

在 Go 1.11 版本(也就是变量名里 111 的由来)之前,Go 的包管理非常反人类,被称为 GOPATH 模式。

旧模式 (GOPATH):

所有的代码项目,必须强制放在一个固定的文件夹里(比如 D:\GoWorks\src)。这就像 C# 强制要求把所有 Visual Studio 的项目都放在C:\Windows\Microsoft.NET\Framework 下面一样荒谬。没有版本控制的概念。go get 下来的库,作者升级了,项目可能就炸了。

新模式 (Go Modules):

也就是 GO111MODULE=on。代码可以放在电脑的任何地方(桌面、D盘、E盘随便)。它使用 go.mod 文件来记录依赖版本(类似于 Python 的 requirements.txt 或 C# 的 .csproj/packages.config)。

项目? go installgo get

如果不创建一个项目的话,vscode右下角会一直爆警告,而且代码补全可能会失效,因为它不知道包依赖关系。刚刚装的gopls就是go语言的IntelliSense引擎。项目也是使用go get安装第三方库要用的,类似于nodejs的package.json

go install会不会就是类似于python的pip install?其实不然

go install是用来装 工具(可执行程序 .exe)的,go get是用来装库(依赖包) 的。

Python的pip是两个都可以装,比如我可以pip install jupyter,然后直接在cmd里使用jupyter notebook启动网页端程序,也可以使用pip install requests装包然后在别的.py里使用

go把它拆开来了

比如刚刚装的gopls,实际就是:先下载gopls的源代码,把它编译成exe后丢进GOPATH里给vscode用

现在的 go get 已经禁止安装可执行文件了,必须用 go install

命令 作用 产物 类比 Python
go install 安装二进制工具 生成 .exe 放到 bin 目录 pip install httpie (为了用命令)
go get 安装开发依赖库 更新 go.mod,下载源码供 import pip install requests (为了写代码)

你好世界

编程惯例写个hello world能提高运气

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("hellooooo worlddddd")
}

使用go build *.go生成二进制文件或者go run *.go直接运行

1
2
3
4
5
PS D:\go-start> go build .\hello.go
PS D:\go-start> .\hello.exe
Hello, World!
PS D:\go-start> go run .\hello.go
Hello, World!

package main 是干嘛的?

Go 的编译器(go buildgo run)需要知道代码的入口在哪里。

一个可执行程序必须有一个名为 main 的包,并且该包里必须包含一个 main() 函数。

如果没有 package main,Go 会把它当成一个库,就不会去找入口函数,也就跑不起来。

别的如果有编程基础都能看懂

变量声明

标准格式

1
2
var hajimi string
var wochao int = 42

变量会被初始化为该类型的零值,也可以在后面加上赋值

短变量声明

1
2
3
4
5
6
7
func main() {
hajimi := ""
wochao := 42

fmt.Println(hajimi, "World!")
_ = wochao
}

使用:=关键字,不需要写类型,go会直接根据内容推导出变量数据类型

但是注意,这样声明变量只能在函数内部。而且golang有个特性,函数内部声明了的变量就必须要用,不然编译会报错。所以最后把wochao赋值给了下划线 _ 占位符

匿名变量

1
2
// 假设函数返回 (int, error)
count, _ := getCount() // 忽略掉第二个返回的 error

如果调用一个返回多个值的函数,但只想用其中一个,可以用下划线 _ 占位。

一次声明多个变量

1
2
3
4
5
6
7
8
9
func main() {
var x, y, z = true, "two", 3 // 不同类型
var a, b, c string // 相同类型
name, age := "关小雨", 18 // 短变量声明

fmt.Println(name, age)
fmt.Println(x, y, z)
c = a + b + c
}

可以通过标准格式和短变量声明一次声明多个变量,变量可以是不同的数据类型

这样运行的输出是:

1
2
关小雨 18
true two 3

批量声明

如果有很多很多变量,可以用括号包起来,通常用于全局变量

1
2
3
4
5
6
7
8
9
10
var (
name string = "关小雨"
age int = 18
sex string = "女"
height float32
)

func main() {
fmt.Println(name)
}

数组

Array

array,顾名思义,数组,和C一样,长度固定的数组

最经典

长度是类型的一部分,且长度固定。下面是最标准的写法,声明时指定长度和类型

1
2
3
4
5
6
7
8
// 声明一个长度为 5 的 int 数组,默认全是 0
var nums [5]int

// 声明并直接初始化
var arr = [3]int{10, 20, 30}

// 短变量声明
names := [2]string{"张三", "李四"}

懒得数了

我懒得数长度了:

1
q := [...]int{1, 2, 3, 4, 5} 

这样编译器会去数个数,长度还是5

指定赋值

可以只给数组里某几个位置赋值,其他位置会自动补零值。

1
2
3
// 索引 1 是 10,索引 3 是 30,长度为 5
a := [5]int{1: 10, 3: 30}
// 结果: [0, 10, 0, 30, 0]

多维数组

用法和别的编程语言一样

1
2
3
4
5
6
// 2行3列
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(matrix[1][2]) // 输出 6

Slice

OK上面关于数组的内容统统没有用,因为我们99%的时间都在用切片,而不是数组。切片是对数组的封装,它的长度可以根据需要动态增长。说人话就是动态数组

创建动态数组

和数组一样,就是不需要指定中括号里的数字

1
2
s1 := []int{1, 2, 3} 
s2 := make([]int, 5, 10)

也可以使用make()并指定预留的长度和初始的数据

上面第二行的意思就是s2有5个0在里面,但是给他预留了10个int的空间

但是切片是动态长度,预留空间何意味?

如果不预留,超出长度的时候,Go 发现没地方了 -> 开辟新内存 -> 搬家 -> 再append ->

又没地方了 -> 再次开辟更大内存 -> 再次搬家

频繁搬家(内存分配和数据复制)非常消耗性能,前 10 次添加数据,Go 发现还有空位,直接放进去就行,不需要搬家。等到第 11 个数据来了,包间满了,Go 才会进行一次大规模搬家(通常会把容量翻倍,比如换个 20 人的大包间)。

设定 cap (容量) 是为了 减少底层数组扩容的次数,提高程序运行速度。虽然不设也能跑,但设了跑得更快。

往里面加东西

通过append可以往动态数组里塞数据

1
2
3
4
5
6
7
s := []int{1, 2}
s = append(s, 3) // 添加一个:[1 2 3]
s = append(s, 4, 5, 6) // 添加多个:[1 2 3 4 5 6]

// 甚至可以把另一个切片直接倒进去(注意后面有三个点)
another := []int{7, 8}
s = append(s, another...) // [1 2 3 4 5 6 7 8]

动态数组的切片

和python的list切片类似,可以从一个现成的数组或动态数组中,截取出一部分。

1
2
3
4
5
6
7
arr := [5]int{0, 1, 2, 3, 4}
s := arr[1:4] // 截取索引 1 到 3 (左闭右开)
// 结果: [1, 2, 3]

// 快捷写法
s = arr[:3] // 从开头到索引 2
s = arr[2:] // 从索引 2 到结尾

(所以我要叫它动态数组,要不然我就要说是切片的切片)

数组和动态数组的传递

数组是值传递,会把值复制一份用于传递。

1
2
3
4
5
6
7
8
func change(a [3]int) {
a[0] = 999 // 这里改的是副本
}
func main() {
nums := [3]int{1, 2, 3}
change(nums)
fmt.Println(nums[0]) // 依然是 1
}

而切片是“引用”传递,实际上是传了一个包含指针的结构体。

1
2
3
4
5
6
7
8
func update(s []int) {
s[0] = 888
}
func main() {
nums := []int{1, 2, 3}
update(nums)
fmt.Println(nums[0]) // 输出 888
}

严格来说,Go 只有值传递。切片本质上是一个只有 3 个字段的结构体 Struct { ptr, len, cap }。当传递切片时,是把这个小结构体复制(值传递)了一份。但因为里面那个 ptr 指针指向了同一个底层数组,所以修改数据会同步。

循环

最经典

1
2
3
for i := 0; i < 5; i++ {
fmt.Println(i)
}

源·while

go里面没有while,但是可以直接把for循环当while用。

也是因为go没有while关键字,所以while在go里甚至可以定义成变量

1
2
3
4
5
while := 0
for while < 5 {
fmt.Println(while)
while++
}

如果什么都不加,直接一个for起手那就是死循环了

次跑循环

类似于python中的for i in range(5),不过格式是这么写的

1
2
3
for i := range 5 {
fmt.Println(i)
}

精准定位

类似于python的

1
2
for i, char in enumerate("你好Go"):
print(f"位置 {i} 的字符是 {char}")

在go里面不需要借助额外的函数,直接这么写:

1
2
3
for i, char := range "你好Go" {
fmt.Printf("位置 %d 的字符是 %c\n", i, char)
}

如果我要遍历数组或者动态数组

1
for i, v := range nums

就当go会展开为

1
2
3
for i := 0; i < len(nums); i++ {
v := nums[i]
}

go的for循环语法糖:

写法 含义
for i := range s 只要索引
for _, v := range s 只要值
for range s 什么都不要,只循环次数

打个坐标

如果有嵌套循环,想在内层循环直接跳出最外层,可以使用标签。

1
2
3
4
5
6
7
8
OuterLoop:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j > 10 {
break OuterLoop // 直接跳出到最外层标签处
}
}
}

不要把break OuterLoop理解为goto OuterLoop,标签只是为了指明作用范围。当执行 break OuterLoop 时,编译器会找到 OuterLoop 标记的那层循环,并立即终止这整层循环

如果是想跳过当前剩余逻辑,直接进入外层循环的下一次迭代,那就和其他语言一样使用continue

Switch

最经典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
x := 2

switch x {
case 1:
fmt.Println("x 等于 1")
case 2:
fmt.Println("x 等于 2")
case 3:
fmt.Println("x 等于 3")
default:
fmt.Println("x 不等于 1, 2 或 3")
}
}

一case多值

我也不知道是不是go的特性

1
2
3
4
5
6
7
8
switch x {
case 1:
fmt.Println("x 等于 1")
case 2, 3, 4, 5, 6, 7, 8, 9:
fmt.Println("x 大于 2 小于 10")
default:
fmt.Println("x 大于 10")
}

也可以这么写,如果switch后面不加变量名,就类似于if-else

1
2
3
4
5
6
7
8
switch {
case x == 1:
fmt.Println("x 等于 1")
case x > 2 && x < 10:
fmt.Println("x 大于 2 小于 10")
default:
fmt.Println("x 大于 10")
}

大炮穿箱

可以使用fallthrough来执行完当前 case 后继续执行下一个 case

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
x := 9

switch {
case x == 1:
fmt.Println("x 等于 1")
case x > 2 && x < 10:
fmt.Println("x 大于 2 小于 10")
fallthrough
default:
fmt.Println("x 大于 10")
}
}

因为在case x > 2 && x < 10:增加了fallthrough,所以下一个default case也被执行了

1
2
x 大于 2 小于 10
x 大于 10

顺手的事

可以在 switch 后面定义一个只在 switch 块内有效的变量。

1
2
3
4
5
6
7
8
9
10
func main() {
switch grade := "B"; grade {
case "A":
fmt.Println("A")
case "B":
fmt.Println("我超 bin")
default:
fmt.Println("铸币吧")
}
}

函数

最经典

Go 的参数名在前,类型在后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 单参数,单返回值
func square(n int) int {
return n * n
}

// 相同类型的参数可以简写
func add(a, b, c int) int {
return a + b + c
}

// 多返回值 上面说过的
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}

味大无需多盐,有其他编程基础的都能看懂

指定返回

在函数头定义返回变量名,函数体里赋值,最后只用写个return就能返回指定变量

1
2
3
4
5
func getRect(width, height int) (area, perimeter int) {
area = width * height
perimeter = (width + height) * 2
return // 自动返回 area 和 perimeter
}

变长参数

... 表示可以传入任意数量的参数。

1
2
3
4
5
6
7
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}

匿名函数

没有名字的函数,通常用于临时逻辑。

1
2
3
4
5
6
func main() {
// 定义并立即执行
func(msg string) {
fmt.Println(msg)
}("Hello Go")
}

方法函数

Go没有类,但可以给结构体绑定函数。这个马上讲结构体会再提这里不展开

1
2
3
4
5
6
7
8
type User struct {
Name string
}

// (u User) 是接收者,表示这个函数属于 User
func (u User) Greet() {
fmt.Println("Hello, I am", u.Name)
}

结构体

Go 没有 class(类),结构体就是 Go 里的“类”。

它不仅能存数据,还能绑定方法,甚至通过“组合”来实现类似继承的功能。

创建结构体

使用 typestruct 关键字。用法和别的编程语言类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type user struct {
name string
sex string
age int
}

func main() {
//键值对初始化
u1 := user{name: "关小雨", sex: "female", age: 18}
fmt.Println(u1.name, u1.sex, u1.age)

//按顺序初始化
u2 := user{"云悠悠", "female", 20}
fmt.Println(u2.name, u2.sex, u2.age)

//什么都不填,全是零值
u3 := user{}
fmt.Println(u3.name, u3.sex, u3.age)

//部分初始化,其他为零值
u4 := user{name: "晴雅"}
fmt.Println(u4.name, u4.sex, u4.age)
}

上面的输出就是

1
2
3
4
关小雨 female 18
云悠悠 female 20
0
晴雅 0

给结构体加函数

就是上文提到的方法函数(Method), Go 的方法定义和 C++ 不一样,它不写在结构体里面,而是写在外面,通过接收者 (Receiver)绑定到结构体上。

格式是这样:

1
func (接收者变量 接收者类型) 方法名(参数) 返回值 { ... }

接收者有两种,值接收者或者指针接收者

(u user):值接收者。相当于把结构体复制了一份传进来。改了它,原来的数据不会变

(u *user):指针接收者,传的是内存地址。改了他原来的数据就变了

还是那个user结构体,给他分别用值接收者和指针接收者加上两个函数

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
type user struct {
name string
sex string
age int
}

func (u user) sayHello() {
fmt.Printf("你好,我是 %s\n", u.name)
//尝试增加年龄
u.age++
fmt.Printf("在 sayHello 方法中,%s 的年龄是 %d\n", u.name, u.age)
}

func (u *user) haveBirthday() {
u.age++
}

func main() {
u1 := user{name: "关小雨", sex: "female", age: 18}
u1.sayHello()
fmt.Printf("%s 现在是 %d 岁\n", u1.name, u1.age)

u1.haveBirthday()
fmt.Printf("%s 现在是 %d 岁\n", u1.name, u1.age)
}

这样的输出结果是

1
2
3
4
你好,我是 关小雨
在 sayHello 方法中,关小雨 的年龄是 19
关小雨 现在是 18 岁
关小雨 现在是 19 岁

嵌套/组合

既然没有继承,怎么复用代码?go提倡 “组合优于继承”。可以把一个结构体塞进另一个结构体里,这叫匿名嵌入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Animal struct {
Type string
}

func (a *Animal) Eat() {
fmt.Println(a.Type, "正在吃东西...")
}

type Dog struct {
Animal // 并没有给字段起名字,直接写类型 -> 这就是嵌入
Name string
}

func main() {
d := Dog{
Name: "旺财",
Animal: Animal{Type: "犬科"},
}

d.Eat() //Dog 也拥有了 Animal 的方法
fmt.Println(d.Name, d.Type, d.Animal.Type) //可以直接访问,也可以通过嵌入类型访问
}

这样的输出结果是

1
2
犬科 正在吃东西...
旺财 犬科 犬科

Dog 并没有成为 Animal 的子类,它只是包含了一个 Animal

Map

go语言的 map 就是哈希表,它是无序(每次遍历 Map 的顺序可能都不一样,Go 故意引入了随机性)的键值对集合。说人话就是python的dict和java的hashmap

声明与初始化

声明 map 后,如果不用 make 初始化,它就是 nil,直接赋值会报错 panic

可以这样用make创建:make(map[Key类型]Value类型)

1
2
age := make(map[string]int) 
age["关小雨"] = 18

也可以通过字面量初始化,声明的时候赋值

1
2
3
4
age := map[string]int{
"关小雨": 18,
"云悠悠": 20, // 最后一个逗号不能少
}

增删改查

无需多盐,字面意思

1
2
3
4
5
6
7
8
9
10
11
12
13
m := make(map[string]string)

// 增 & 改
m["name"] = "关小雨" // 新增
m["name"] = "云悠悠" // 修改,覆盖旧值

// 查
name := m["name"]
fmt.Println(name) // 云悠悠

// 删
delete(m, "name") // 删除键 "name"
// 如果删除一个不存在的键,Go 不会报错,什么都不发生

查不到啊

这是 map 独特的特性。去拿一个不存在的 Key 时,不会报错,而是返回该类型的零值(比如 int 返回 0,string 返回 “”)。
但这有个坑:不知道返回的 0 是因为它真的存了 0,还是因为根本没这个 Key。

解决方案:双返回值判断

1
2
3
4
5
6
7
8
9
10
func main() {
ages := make(map[string]int)

age, ok := ages["关小雨"]
if ok {
fmt.Println("关小雨的分数是", age)
} else {
fmt.Println("查无此人")
}
}

ok 是一个布尔值,true 表示存在,false 表示不存在。

Key能用的类型?

只要能用 == 比较的类型,都能当 Key。

可以:bool, int, float, string, 指针, channel, 接口

不可以:动态数组, map, 函数

错误处理

众所周知,Go 语言没有 try-catch-finally 这种异常处理机制。

Go 的设计哲学是:错误也是一种值。它不希望把错误“抛出”到一个不可预知的地方,而是希望原地处理或者显式地传递它。

在继续错误处理之前,得知道这么一个go的关键字:nil

nil

nil 是 go 语言中某些特定类型的默认零值。可以把它类比为其他语言中的 nullNone
在 Go 中,这些类型的“空值”都是 nil:接口,指针 ,切片,映射,通道和函数。

但是go的基本类型不能是nil而是默认值:

int 的零值是 0

string 的零值是 ""(空字符串),不是 nil

bool 的零值是 false

这样就杜绝了像 C 语言中“这到底是数字 0 还是空指针”的二义性。

多返回值

让函数在返回结果的同时,多返回一个 error 类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func doSomething() (int, error) {
chushi := true
if chushi {
return 0, fmt.Errorf("出事了")
} else {
return 100, nil
}
}

func main() {
res, err := doSomething()
if err != nil {
// 原地处理错误
fmt.Println("报错:", err)
return
}
// 没报错才继续
fmt.Println("结果是:", res)
}

如果返回的error不是nil,那就出大事了

添加信息

给底层的错误加点描述,再往上传,可以使用 %w

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func openConfig(filepath string) error {
_, err := os.Open(filepath)
if err != nil {
// 使用 %w 包装原始错误
return fmt.Errorf("读取配置文件失败: %w", err)
}
return nil
}

func main() {
err := openConfig("config.yaml")
if err != nil {
fmt.Println("出事了", err)
}
}

这样的输出就是

1
出事了 读取配置文件失败: open config.yaml: The system cannot find the file specified.

扣1复活

Go 虽然没有 try-catch,但有 panic-recover 机制。这不是用来处理普通业务错误的,而是用来处理毁灭性错误(比如数组越界、空指针、数据库连接不上)。

1
2
3
4
5
6
7
8
9
10
11
12
13
func protect() {
defer func() {
fmt.Println("1111 复活成功并且捕捉到了崩溃:", recover())
}()

fmt.Println("准备开始搞破坏...")
panic("坠机了") // 相当于 throw
}

func main() {
protect()
fmt.Println("程序继续运行...")
}

defer这个关键字的意思是:它后面的函数在当前函数退出前的最后一刻,一定会执行。哪怕函数崩溃了,defer 里的内容也会被执行。

recover是用来”复活”程序的,只能写在defer里。用于获取到出的问题。recover 不仅能捕获手动写的 panic,还能捕获go运行时触发的“运行时错误”(Runtime Error)。

panic是用来手动触发一个错误的函数

这样的执行结果是:

1
2
3
准备开始搞破坏...
1111 复活成功并且捕捉到了崩溃: 坠机了
程序继续运行...

自定义error

在 Go 中,任何实现了Error() string方法的类型都可以是一个错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type MyError struct {
Code int
Message string
}

func (e *MyError) Error() string {
return fmt.Sprintf("错误码:%d, 消息:%s", e.Code, e.Message)
}

func getAPI() error {
return &MyError{Code: 404, Message: "找不到了"}
}

func main() {
err := getAPI()
if err != nil {
fmt.Println("API调用失败:", err)
}
}

这个后文讲鸭子类型的时候会再翻出来解析

接口/鸭子类型

如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。

在 Go 里:只要一个类型实现了接口要求的所有方法,它就自动属于这个接口,不需要显式写 implements

创建和使用

先这样定义一个鸭叫接口

1
type Quack interface{ Quack() }

但是不止鸭子能叫,人也能学鸭子叫。所以再定义人和鸭的结构体并实现Quack函数

1
2
3
4
5
type duck struct{ name string }
func (d duck) Quack() { fmt.Println(d.name, "嘎嘎嘎") }

type person struct{ name string }
func (p person) Quack() { fmt.Println(p.name, "学鸭子叫:嘎嘎嘎") }

现在再来创建一个用于调用鸭叫接口的函数

1
func makeQuack(q Quack) { q.Quack() }

person和duck都可以调用这个鸭叫接口函数

1
2
3
4
5
6
7
func main() {
d1 := duck{name: "唐老鸭"}
p1 := person{name: "张三"}

makeQuack(d1)
makeQuack(p1)
}

输出是:

1
2
唐老鸭 嘎嘎嘎
张三 学鸭子叫:嘎嘎嘎

空接口

go中有一个特殊的接口:any,没有定义任何方法。既然没有任何要求,那就意味着:任何类型都实现了空接口。它可以装万物

1
2
3
4
5
6
7
8
9
10
11
12
type duck struct{ name string }

func main() {
var i any = 10 // 可以装 int
fmt.Println(i)

i = "Hello" // 可以装 string
fmt.Println(i)

i = duck{name: "Daffy"} // 可以装 duck 结构体
fmt.Println(i.(duck).name)
}

这样的输出结果是:

1
2
3
10
Hello
Daffy

指针

Go 的指针在语法上和 C/C++ 非常像,但在逻辑上被“阉割”,为了安全和简单。

没有指针运算,没有复杂的内存管理,没有 -> 箭头符号

使用指针

和C/C++一样。& (取地址),获取变量的内存地址。* (解引用),获取指针指向的值,或者定义指针类型。

1
2
3
4
5
6
7
8
9
10
func main() {
a := 10
ptr := &a

fmt.Println(ptr) // 0xc00000a0c8 内存地址
fmt.Println(*ptr) // 10

*ptr = 20
fmt.Println(a) // 20
}

结构体指针的“语法糖”

在 C++ 中,访问指针对象的字段要用箭头 p->age。在 Go 中,统一都用点 .。编译器会分辨是值还是指针。

1
2
3
4
5
6
7
8
9
10
11
type User struct {
Name string
Age int
}

func main() {
u := User{Name: "关小雨", Age: 18}
ptr := &u
ptr.Name = "云悠悠" // 编译器自动识别 ptr 是指针,自动解引用
fmt.Println(u.Name)
}

使用场景

需要修改外部变量

go函数默认是值传递(拷贝)。如果想在函数里修改外面的变量,必须传指针。

1
2
3
4
5
6
7
8
9
// 接收指针类型 *int
func change(n *int) {
*n = 999
}
func main() {
x := 10
change(&x) // 传地址进去
fmt.Println(x) // 999
}

避免大对象拷贝

如果结构体很大(比如有 100 个字段),传值会发生一次巨大的内存拷贝。传指针只需要拷贝 8 个字节(64位系统),速度快很多。

总而言之

特性 C++ 指针 Go 指针
定义符号 * *
取址符号 & &
成员访问 -> (指针), . (对象) 统一用 .
指针运算 支持 (p++, p+1) 不支持 (报错)
野指针/释放 需要手动 delete GC 自动回收,不用管
安全性 危险 (易越界/内存泄漏) 相对安全

public?private?

虽然我觉得这么做有点逆天但是,在 Go 中,首字母大写类似于Public,首字母小写类似于Private。不仅仅是变量名,还包括函数名、结构体名、结构体内部的字段名

为什么说类似,因为这个public和private都指的是能不能跨包访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"encoding/json"
"fmt"
)

type User struct {
name string
age int
}

func main() {
u := User{name: "关小雨", age: 18}
data, _ := json.Marshal(u)
fmt.Println(string(data))
}

这样运行,输出是空的,json.Marshal 是 json 包里的函数,它是“外人”,它看不到 User 里没导出的 name 和 age。

但是如果把结构体变量名首字母都改成大写,就正常了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"encoding/json"
"fmt"
)

type User struct {
Name string
Age int
}

func main() {
u := User{Name: "关小雨", Age: 18}
data, _ := json.Marshal(u)
fmt.Println(string(data))
}

运行正常输出

1
{"Name":"关小雨","Age":18}

Congratulations

恭喜,看完本文你已经了解了其他编程语言到golang的迁移

你可能在找 Goroutine Channel?Go 最强的并发特性留到下一篇专门讲。