Golang 程序结构¶
变量¶
变量类型¶
Golang 支持以下几种变量类型:
-
布尔类型:
bool -
整数类型:
int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr -
浮点数类型:
float32,float64 -
复数类型:
complex64,complex128 -
字符串类型:
string
详情可参考基本类型 | Go语言中文文档。
变量声明¶
在Golang中,变量声明有三种方式:
-
使用
var关键字声明变量-
显式声明变量类型
var identifier1, identifier2 type = value1, value2 -
隐式判定
var identifier1, identifier2 = value1, value2
-
-
使用
:=关键字声明变量(初始化声明)identifier1, identifier2 := value1, value2这种声明方式类似C++中的auto关键字,是声明变量的首选方式,但只能用于局部变量的声明。
注意使用这种方式时不能重复声明,否则会产生编译错误。
多变量声明¶
除了上面提到的三种基本方式的变种外,还可以通过下面的方式声明变量:
var (
value1 type1
value2 type2
)
这种声明方式一般用于声明全局变量。
变量的生命周期¶
package main
import "fmt"
var globalStr string
var globalInt int
func main() {
var localStr string
var localInt int
localStr = "first local"
localInt = 2021
globalInt = 1024
globalStr = "first global"
fmt.Printf("globalStr is %s\n", globalStr) //globalStr is first global
fmt.Printf("globalStr is %d\n", globalInt) //globalStr is 1024
fmt.Printf("localInt is %s\n", localStr) //localInt is first local
fmt.Printf("localInt int is %d\n", localInt) //localInt int is 2021
}
常量¶
与其他主流语言类似,Golang也使用 const 关键字声明常量:
const identifier1, identifier2 [type] = value1, value2
常量不可以在运行时被修改。
常量声明可以以编译期中可以确定结果的表达式作为右值:
const (
str = "hello world"
strLength = len(str)
strUpper = strings.ToUpper(str)
)
常量用作枚举¶
Golang与C++不同,没有枚举类型,但可以使用常量来实现枚举:
const (
Unknown = 0
Success = 1
Failure = -1
)
iota¶
iota是Golang中的一个常量计数器,用于生成递增的常量值,通常用于枚举类型的定义。iota可简单理解为可以被编译器修改的常量。
iota在const块中被重置为0,每次遇到新的const块时,iota都会被重置为0。
const (
Unknown = iota // 0
Success = iota // 1
Failure = iota // 2
)
在新的一行中,iota会继续递增,因此下面的代码与上面等效:
const (
Unknown = iota // 0
Success // 1
Failure // 2
)
注意变量本身的值与iota是相互独立的,可通过以下示例验证:
package main
import "fmt"
func main() {
const (
a = iota // 0
b // 1
c // 2
d = "ha" // 独立值,iota = 3
e // "ha",iota = 4
f = 100 // iota = 5
g // 100,iota = 6
h = iota // 7,恢复计数
i // 8
)
fmt.Println(a, b, c, d, e, f, g, h, i) // 0 1 2 ha ha 100 100 7 8
}
运算符¶
与其他主流静态类型语言没什么不同。可参考Go运算符 | Go语言进阶之路
结构体¶
与C++中的结构体类似,Golang中的结构体也是一种复合数据类型,用于表示一组相关数据的集合。
type Person struct {
Name string
Email string
}
// 键值对初始化
person := Person{
Name: "John",
Email: "john@example.com"
}
// 列表初始化
person := Person{
"John",
"john@example.com"
}
访问也与C++中的结构体类似,使用.运算符访问结构体的成员:
fmt.Println(person.Name) // output: John
fmt.Println(person.Email) // output: john@example.com
数组与切片¶
数组¶
在Golang中,数组用于存放一系列同类型的数据。
有多种方式定义数组:
// 1. 最基础的定义方式
var scores [3]int // 定义一个能存放3个整数的数组
// 2. 定义时直接赋值
var prices = [3]float64{10.99, 20.99, 30.99}
// 3. 让编译器自动计算长度
names := [...]string{"张三", "李四", "王五"}
// 4. 指定特定位置的值
colors := [5]string{0: "红", 2: "蓝", 4: "绿"} // [红 "" 蓝 "" 绿]
Golang的数组是定长数组,一旦定义,数组长度不可变。同时,在Golang中,数组的长度是类型的一部分,因此[3]int与[4]int是两种不同的类型。
切片¶
切片(Slice)可以看作是一种动态数组,其长度可变。
切片是基于数组实现的,因此切片的长度不能超过其底层数组的长度。
创建切片的方式也有多种:
// 1. 从数组创建
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // [2, 3, 4]
// 2. 直接创建
s2 := []int{1, 2, 3}
// 3. 使用 make 创建
s3 := make([]int, 3, 5) // 长度3,容量5
切片包含指向底层数组的指针、长度和容量。
切片是引用类型,多个切片可共享同一底层数组,修改一个可能影响另一个。
基本操作¶
s := []int{1, 2, 3}
s = append(s, 4) // 追加元素
fmt.Println(len(s)) // 长度: 4
fmt.Println(cap(s)) // 容量: 可能大于长度
append可能导致重新分配内存,生成新的底层数组;使用make创建切片时,可以指定容量来减少重新分配内存的次数。
映射¶
在Golang中,映射(Map)是一种无序的键值对集合,用于存储和检索数据,类似Python中的字典(dict)与数据结构中的哈希表的具象化。
定义映射¶
// 1. 使用make函数创建
scoreMap := make(map[string]int, 100) // 预分配100个键值对空间
scoreMap := make(map[string]int) // 不预分配空间
// 2. 创建时直接初始化
studentScores := map[string]int{
"张三": 95,
"李四": 88,
"王五": 92,
}
// 3. 声明一个空map
var prices map[string]float64
// 注意:声明后需要通过make初始化才能使用
prices = make(map[string]float64)
基本操作¶
// 1. 添加或修改键值对
scoreMap["王五"] = 92
// 2. 获取值
score := scoreMap["张三"]
fmt.Println(score) // output: 95
// 3. 删除键值对
delete(scoreMap, "张三")
// 4. 遍历
for key, value := range scoreMap {
fmt.Println(key, value)
}
嵌套映射¶
映射可以嵌套,例如:
score := map[string]map[string]int{
"张三": {
"语文": 95,
"数学": 88,
"英语": 92,
},
"李四": {
"语文": 88,
"数学": 95,
"英语": 92,
},
}
fmt.Println(score["张三"]["语文"]) // output: 95
作为函数参数¶
映射是引用类型,在作为函数参数传递时传递的是底层指针的拷贝
func updatePrice(prices map[string]float64, fruit string, price float64) {
prices[fruit] = price
}
一些注意事项¶
-
map的零值是nil,未初始化的map不能直接使用,需要先初始化 -
不能对
map元素进行取址操作 -
map的遍历顺序是随机的,即键值对的顺序不固定
并发安全¶
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// 不安全的map操作
unsafeMap := make(map[int]int)
// 使用WaitGroup等待所有goroutine完成
var wg sync.WaitGroup
// 启动10个goroutine同时写map
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
unsafeMap[n] = n // 可能导致panic
time.Sleep(time.Millisecond)
}(i)
}
// 安全的map操作
var mutex sync.Mutex
safeMap := make(map[int]int)
// 再次启动10个goroutine,这次使用互斥锁保护
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
mutex.Lock()
safeMap[n] = n // 安全的写入
mutex.Unlock()
time.Sleep(time.Millisecond)
}(i)
}
wg.Wait()
fmt.Printf("安全的map最终内容: %v\n", safeMap)
}
使用sync.Map:
package main
import (
"fmt"
"sync"
)
func main() {
var sm sync.Map
var wg sync.WaitGroup
// 启动10个goroutine同时操作sync.Map
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sm.Store(n, n) // 存储键值对
}(i)
}
wg.Wait()
// 遍历sync.Map
sm.Range(func(key, value interface{}) bool {
fmt.Printf("key: %v, value: %v\n", key, value)
return true
})
}
条件语句¶
if语句¶
if condition {
// code
} else if condition {
// code
} else {
// code
}
Golang中的if语句支持初始化语句:
if statement; condition {
// code
}
switch语句¶
switch expression {
case value1, value2, ...:
// code
case value3, value4, ...:
// code
...
default:
// code
}
Golang中,通过关键字fallthrough可以实现穿透效果,即执行完当前case后继续执行下一个case,不管下一个case的条件是否满足:
switch statement {
case value1, value2, ...:
// code
fallthrough
case value3, value4, ...:
// code
...
default:
// code
}
循环语句¶
for 循环¶
Golang中只有for循环,但是有三种形式:
for init; condition; post {
// code
}
for condition {
// code
}
for {
// code
}
for range¶
range 的对象可以是切片、映射、数组、字符串。
for key, value := range oldMap {
newMap[key] = value
}
// 只获取 key
for key := range oldMap {
newMap[key] = value
}
// 只获取 value
for _, value := range oldMap {
newMap[key] = value
}