如果使用 VSCode,需要安装两个插件:Go、Golang
Hello World
-
初始化项目
mkdir hello cd hello go mod init github.com/[你的id]/hello touch main.go -
编写代码
进入 main.go
package main import "fmt" func main() { fmt.Println("Hello World!") } -
编译并执行
go build . ./hello # "Hello World!" # 也可以只运行 go run . # "Hello World!"
变量、常量、iota
变量
// 声明类型
var b int = 2
var c, d int = 3, 4
var (
e = 5
f, g int = 6, 7
// 不需要 var 关键字,并且自动指定类型
h := 8
i, j := 9, 10
)
常量与 iota
const (
a1 = iota // iota 是从 0 开始
a2
a3
)
iota
- iota 即希腊字母 ι
- iota 是其英文发音
- GoLang 中 iota 默认为 0,每行加 1
- 使用 iota 可以减少 hard code(写死的代码)
- 只能用在 const () 中
for 循环
// 接 1 个表达式
i := 1
for i <= 3 {
fmt.Println(i)
i++
}
// 3 个表达式
for j := 1; j <= 3; j++ {
fmt.Println(j)
}
// 接 0 个表达式
k := 1
for {
if k > 3 {
break
}
fmt.Println("可能无限循环")
k++
}
与 JS 不一样的地方:
- 不加 ( ),加了会自动删
- for 接 0 个表达式等价于 JS 的 while(true)
- for 接 1 个表达式等价于 JS 的 while(condition)
- for 接 3 个表达式等价于 JS 的 for(初始化, 判断, 后续)
if else
if num := 9; num < 0 {
fmt.Println(num, "负数")
} else if num < 10 {
fmt.Println(num, "一位数")
} else {
fmt.Println(num, "多位数")
}
与 JS 不一样的地方:
- 推荐不加 ( )
- if 接一个表达式等价于 JS 的 if(condition)
- if 接两个表达式没有等价的 JS 常见写法
switch
switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
fmt.Println("休息日")
default:
fmt.Println("工作日")
}
// switch case 判断类型
var i any = true
switch t := i.(type) {
case bool:
fmt.Println("布尔")
case int:
fmt.Println("整数")
default:
fmt.Printf("未知类型 %T\n", t)
}
与 JS 不一样的地方:
- 不推荐加 ( )
- 不要加 break
- 一个 case 可以有多个值,用逗号隔开
函数
普通函数
func f1(x, y int) (int, int) {
return x + y, x * y
}
func f2(x, y int) (sum, product int) {
sum = x + y
product = x * y
return
}
func main() {
a, b := f1(3, 4)
c, d := f2(5, 6)
fmt.Println(a, b, c, d)
}
可变参数函数
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2))
fmt.Println(sum(1, 2, 3))
nums := []int{1, 2, 3, 4}
fmt.Println(sum(nums...))
}
匿名函数
func main() {
sum := func(a, b int) int {
return a + b
}
fmt.Println(sum(1, 2))
}
func(c int) {
fmt.Println(c)
}(3)
与 JS/TS 不一样的地方:
- 返回值可以提前定义名字,return 可缩写
- numbers ...int 表示多个 int 参数组成的数组
- 立即执行函数不需要 Hack
数据类型
简单值类型
- 数字(14 种):int32、float64
- 字符串
- 布尔
复杂值类型
- 结构体(struct)
- 数组(定长):[3]int
引用类型
该类型的变量不直接存放值,而是存放值的地址和其他信息
- 指针:*int
- 切片:[]int
- 哈希表:map[string]int
- 函数
- 通道:chan int
- 接口:interface {}
结构体
type Point struct{ X, Y int }
p1 := Point{1, 2}
fmt.Println(p1.X, p1.Y)
p2 := Point{X: 3, Y: 4}
fmt.Println(p2.X, p2.Y)
把结构体当做参数 1
type Point struct{ X, Y int }
func modify(p Point) {
p.X = 42
}
func main() {
p1 := Point{1, 2}
modify(p1)
fmt.Println(p1)
}
* 与 &
用于类型:
- var a *int 表示 a 存 int 的地址
- 此时称 a 为指针
用于值:
- &b 表示 b 的地址
- *c 表示指针 c 对应的值
我个人认为,用 var a &int 表示 a 存 int 的地址,更符合语义
把结构体当做参数 2
type Point struct{ X, Y int }
func modify(p *Point) {
(*p).X = 42 // 与 p.X = 42 等价,语法糖
}
func main() {
p1 := Point{1, 2}
modify(&p1)
fmt.Println(p1)
}
结构体变字符串
type User struct {
ID string `json:"id"`
UserName string `json:"username"`
Email string `json:"email"`
}
func main() {
u := User{
ID: "1",
UserName: "test",
Email: "test@test.com",
}
bytes, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}
结构体的特点
与 JS 的不同之处:
- 结构体是值类型,不是引用类型,不能与 JS 的对象进行类比
- Go 只支持传值,不过可以把地址当做值
modify(&p1) - 结构体支持 label,用于各种功能;JS 没有 label
数组(定长)
a := [5]int{1, 2, 3}
a[1] = 20
fmt.Println(a) // [1 20 3 0 0]
与 JS 的不同之处:
- 数组是值类型,不是引用类型,不能与 JS 的数组进行类比
- 数组长度是固定的
- 可以用 len(a) 获取数组的长度,而不是 a.length
指针 Pointer
func zeroValue(ival int) { ival = 0 }
func zeroPointer(iptr *int) { *iptr = 0 }
func main() {
i := 1
iPtr := &i
fmt.Println("i: ", i)
fmt.Println("iPtr :", &i)
zeroValue(i)
fmt.Println("zeroValue(i), i =", i)
zeroPointer(iPtr)
fmt.Println("zeroPointer(&i), i =", i)
}
与 JS 的不同之处:
- JS 没有指针(无法获取或者打印变量的地址),只有引用(别名)
- &i 表示 i 的地址,*p 表示指针 p 对应的值
- 地址一般用十六进制数表示(0xFFFFFFFFF)
- 理论上可以对指针进行加减操作,但是 Go 默认关闭了此功能(见下页)
unsafe 不安全
vals := []int{10, 20, 30, 40}
start := unsafe.Pointer(&vals[0])
size := unsafe.Sizeof(int(0))
for i := 0; i < len(vals); i++ {
item := *(*int)(unsafe.Pointer(
uintptr(start) + size*uintptr(i)
))
fmt.Println(item)
}
切片(可扩容)
a := [3]int{1, 2, 3}
a2 := [...]int{1, 2, 3}
s := []int{1, 2, 3}
s2 := make([]int, 3)
ta := reflect.TypeOf(a)
fmt.Println(ta.Kind())
// array
ta2 := reflect.TypeOf(a2)
fmt.Println(ta2.Kind())
// array
ts := reflect.TypeOf(s)
fmt.Println(ts.Kind())
// slice
ts2 := reflect.TypeOf(s2)
fmt.Println(ts2.Kind(), s2)
// slice [0 0 0]
追加元素
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Printf("s: %v, &s: %p\n", s, &s)
s = append(s, "d")
fmt.Printf("s: %v, &s: %p\n", s, &s)
s = append(s, "e", "f")
fmt.Printf("s: %v, &s: %p\n", s, &s)
扩容
var s []bool
for i := 0; i < 100; i++ {
s = append(s, true)
fmt.Printf("cap: %v, address: %p\n", cap(s), s)
}
切取
a := [...]int{1, 2, 3, 4, 5}
s1 := a[0:3]
s2 := s1[0:1]
fmt.Println(s1, s2)
切片的特点
- 每个 slice 都含有一个底层数组、一个长度和一个容量
- slice 本身只是一个结构体而已
- 使用 append 向切片追加元素时可能触发扩容,得到新的切片
数据所占字节数
获取
方法一:
var x int
t := reflect.TypeOf(x)
fmt.Println(t, ":", t.Size(), "字节")
// int : 8 字节
方法二:
var x int
fmt.Printf("%T: %d 字节\n", x, unsafe.Sizeof(x))
// int: 8 字节
数据所占字节数表格:
| 数据类型 | 占用字节数 | 占用位数 | 说明 |
|---|---|---|---|
| int | 8 | 64 | 跟操作系统位数有关 |
| bool | 1 | 8 | |
| string | 16 | 64 * 2 | 内容指针 + length |
| 数组 | size * len | size * len * 8 | |
| 函数 | 8 | 64 | |
| 指针 | 8 | 64 | |
| 通道 | 8 | 64 | |
| map | 8 | 64 | |
| interface | 16 | 64 * 2 | 内容指针 + itab 指针 |
| 切片 | 24 | 64 * 2 | 数组指针 + length + capacity |
| 结构体 | 要算 | 要算 | 需要考虑内存对齐 |
更多学习资料
-
Go 官方指南
tour.go-zh.org -
Go by Example
gobyexample-cn.github.io -
Go 官方文档
go.dev/doc -
幼麟实验室
BV1hv411x7we -
深入理解 GPM 模型
BV19r4y1w7Nx