我们来逐步学习Go语言的基本语法和数据类型
目录
我们从第一个知识点开始学习:变量。
1. 变量
变量是计算机中存储和操作数据的基本单位。在Go中,变量必须先声明后使用。变量声明的基本语法如下:
var 变量名 变量类型
其中,变量名是标识符,表示变量的名称;变量类型表示变量所存储的数据类型。
例如,以下代码声明了一个整型变量a和一个字符串变量b:
var a int
var b string
我们也可以一次声明多个变量,如下所示:
var a, b, c int
在Go中,还可以使用简短声明语法:=
,它可以自动推断变量类型并进行赋值,例如:
a := 10
b := "hello"
这个语法等价于以下代码:
var a int = 10
var b string = "hello"
注意,简短声明语法只能用于函数内部,并且只能用于声明新的变量,不能用于修改已有的变量。
2. 常量
常量是一种在程序运行期间不会被修改的值。在Go中,常量使用const
关键字声明,其基本语法如下:
const 常量名 = 值
其中,常量名是标识符,表示常量的名称;值表示常量的值。
例如,以下代码声明了两个常量Pi和Max:
const Pi = 3.1415926
const Max = 100
在Go中,常量可以是字符串、布尔值、数字等类型,也可以是枚举类型的值。常量的值必须在编译时确定,不能在运行时修改。
Go中还支持枚举类型的定义,可以通过const
关键字实现。例如,以下代码定义了一个枚举类型Weekday:
const (
Sunday = 0
Monday = 1
Tuesday = 2
Wednesday = 3
Thursday = 4
Friday = 5
Saturday = 6
)
在Go中,常量的值可以是表达式,但是表达式的值必须在编译时能够确定。例如,以下代码定义了一个常量SecondsPerDay:
const SecondsPerDay = 24 * 60 * 60
在编译时,表达式24 * 60 * 60会被求值为86400,所以SecondsPerDay的值是86400。
3. 基本数据类型
Go 语言中最基础的数据类型,包括布尔类型、数字类型和字符串类型。下面具体介绍一下每个类型:
-
布尔类型(bool):布尔类型只有两个值,true 和 false。用于表示逻辑真和逻辑假。例如:
var b bool = true
-
数字类型:Go 语言提供了多种数字类型,包括整型和浮点型。
-
整型:Go 语言提供了有符号和无符号两种整型,每种类型有不同的长度。例如:
var i8 int8 = -128 // 有符号 8 位整型,范围为 -128 到 127 var i16 int16 = -32768 // 有符号 16 位整型,范围为 -32768 到 32767 var i32 int32 = -2147483648 // 有符号 32 位整型,范围为 -2147483648 到 2147483647 var i64 int64 = -9223372036854775808 // 有符号 64 位整型,范围为 -9223372036854775808 到 9223372036854775807 var u8 uint8 = 255 // 无符号 8 位整型,范围为 0 到 255 var u16 uint16 = 65535 // 无符号 16 位整型,范围为 0 到 65535 var u32 uint32 = 4294967295 // 无符号 32 位整型,范围为 0 到 4294967295 var u64 uint64 = 18446744073709551615 // 无符号 64 位整型,范围为 0 到 18446744073709551615
-
浮点型:Go 语言提供了两种浮点型,分别是 float32 和 float64。例如:
var f32 float32 = 1.23456789 // 32 位浮点型,范围为 1.18e-38 到 3.4e38,精度为小数点后 7 位 var f64 float64 = 1.2345678901234567 // 64 位浮点型,范围为 2.23e-308 到 1.8e308,精度为小数点后 15 位
-
-
字符串类型:字符串类型用于表示文本。在 Go 语言中,字符串是不可变的,也就是说一旦创建就无法修改。例如:
var str string = "Hello, world!"
-
其他类型:还有其他一些基本数据类型,包括复数类型、字节类型和指针类型等,但使用较为少见。
4. 数组
- 数组的定义和初始化
数组是一组具有相同数据类型的元素的集合,每个元素都有一个唯一的下标(从0开始)。Go语言中的数组定义格式如下:
var arrayName [size]dataType
其中,arrayName表示数组名,size表示数组的长度,dataType表示数组元素的数据类型。数组的初始化有两种方式,一种是通过指定下标进行初始化,另一种是使用初始化表达式列表初始化。如下所示:
var a [3]int // 声明一个长度为3的int类型数组a
a[0] = 1 // 初始化数组元素
a[1] = 2
a[2] = 3
b := [3]int{1, 2, 3} // 声明并初始化一个长度为3的int类型数组b
- 数组的遍历
数组的遍历可以使用for循环,也可以使用range关键字。例如:
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
for i, v := range a {
fmt.Printf("a[%d] = %d\n", i, v)
}
其中,len()函数用于获取数组的长度,i表示数组元素的下标,v表示数组元素的值。
- 数组的传递
在Go语言中,数组是值类型。如果将一个数组作为参数传递给函数,那么实际上传递的是该数组的一个副本,而不是原数组。如果需要在函数内部修改原数组,可以使用数组指针。例如:
func modifyArray(a *[3]int) {
(*a)[0] = 10
}
func main() {
a := [3]int{1, 2, 3}
modifyArray(&a)
fmt.Println(a) // 输出 [10 2 3]
}
- 多维数组
Go语言支持多维数组。例如,一个二维数组的定义格式如下:
var arrayName [size1][size2]dataType
其中,size1表示数组的第一维长度,size2表示数组的第二维长度,dataType表示数组元素的数据类型。多维数组的遍历可以使用嵌套循环。例如:
var a [3][2]int = [...][2]int{{1, 2}, {3, 4}, {5, 6}}
for i := 0; i < len(a); i++ {
for j := 0; j < len(a[i]); j++ {
fmt.Printf("a[%d][%d] = %d\n", i, j, a[i][j])
}
}
5. 切片
切片是 Go 语言中重要的数据结构之一,它是一个由相同类型元素构成的可变长度的序列。
切片有以下特点:
-
与数组相比,切片的长度是可变的。
-
切片是一个引用类型,它的零值为
nil
。 -
切片底层引用一个数组,切片的实际容量可能大于其长度。
-
通过切片的表达式可以重新分配切片所引用的数组大小,因此可以使用切片来实现动态数组的功能。
下面是一些关于切片的常用操作:
- 创建一个切片
slice := make([]int, 3) // 创建一个长度为3的int型切片
- 切片表达式
a[low : high] // [low, high) 的半开区间
a[low : ] // 从 low 到切片的末尾
a[: high] // 从切片的开头到 high-1
a[ : ] // 全部
- 获取切片的长度和容量
len(slice) // 获取切片的长度
cap(slice) // 获取切片的容量
- 向切片中追加元素
slice = append(slice, elem1, elem2, ...) // 将元素追加到切片的末尾
- 复制切片
copy(destSlice, srcSlice) // 将 srcSlice 复制到 destSlice 中
- 删除切片中的元素
// 从切片中删除一个元素
slice = append(slice[:index], slice[index+1:]...)
以上是一些常用的切片操作,当然,还有很多其他的操作和技巧,需要在实际的开发中进行实践和探索。
6. map
Map是一种键值对的集合类型,类似于Java中的HashMap或Python中的字典。其中,键和值可以是任意类型的数据,不同于数组或切片,它是一个无序的集合类型。
在Go语言中,使用make
函数来创建一个map,语法如下:
mapName := make(map[keyType]valueType)
其中,mapName
表示要创建的map的名称,keyType
表示map中键的数据类型,valueType
表示map中值的数据类型。
例如,我们可以创建一个键为字符串类型,值为整数类型的map,如下所示:
scoreMap := make(map[string]int)
向map中添加元素可以使用mapName[key] = value
的方式,如下所示:
scoreMap["Alice"] = 90
scoreMap["Bob"] = 80
scoreMap["Cindy"] = 70
我们也可以使用delete
函数删除map中的元素,语法为delete(mapName, key)
。例如,我们可以删除scoreMap
中的"Cindy"键:
delete(scoreMap, "Cindy")
可以使用len
函数获取map中键值对的数量:
length := len(scoreMap)
遍历map可以使用range
关键字,如下所示:
for key, value := range scoreMap {
fmt.Println(key, value)
}
这个循环将会输出scoreMap
中每个键和对应的值。如果我们只需要遍历键或值,可以忽略其中一个变量:
for key := range scoreMap {
fmt.Println(key)
}
for _, value := range scoreMap {
fmt.Println(value)
}
7. 结构体
结构体是一种自定义的复合数据类型,它可以包含多个字段。在Go语言中,结构体的声明格式如下:
type 结构体名 struct {
字段1 数据类型1
字段2 数据类型2
...
}
例如,声明一个包含姓名和年龄两个字段的结构体:
type Person struct {
Name string
Age int
}
结构体中的字段可以使用点号访问:
person := Person{Name: "张三", Age: 20}
fmt.Println(person.Name) // 输出:张三
fmt.Println(person.Age) // 输出:20
可以使用new函数创建一个结构体的指针:
person := new(Person)
person.Name = "张三"
person.Age = 20
fmt.Println(person.Name) // 输出:张三
fmt.Println(person.Age) // 输出:20
结构体可以嵌套:
type Address struct {
Province string
City string
}
type Person struct {
Name string
Age int
Address Address
}
可以使用匿名字段简化结构体的定义:
type Address struct {
Province string
City string
}
type Person struct {
Name string
Age int
Address
}
接下来我们学习方法。
8. 结构体
在Go语言中,方法是与特定类型相关联的函数。它们可以被视为该类型的行为。方法与函数的区别在于,方法是一个函数与一个接收器(receiver)绑定在一起的。这个接收器是一个命名类型的值或者指针。方法的定义格式如下:
func (接收器变量 接收器类型) 方法名(参数列表) (返回值列表) {
// 方法体
}
其中,接收器变量在方法被调用时会将调用者的值或指针复制给它,方法可以在其上进行操作。接收器类型可以是结构体类型或非结构体类型,但是必须是当前包内定义的类型。
例如,我们可以定义一个Person类型,并为它添加一个方法SayHello:
type Person struct {
Name string
Age int
}
func (p Person) SayHello() {
fmt.Printf("你好,我是%s,今年%d岁。\n", p.Name, p.Age)
}
func main() {
person := Person{Name: "张三", Age: 20}
person.SayHello() // 输出:你好,我是张三,今年20岁。
}
注意,如果想要在方法中修改接收器的值,需要使用指针类型的接收器:
func (p *Person) GrowUp() {
p.Age++
}
func main() {
person := &Person{Name: "张三", Age: 20}
person.GrowUp()
fmt.Println(person.Age) // 输出:21
}
此外,Go语言还有一种特殊类型的方法,叫做接口。我们将在下一个知识点中学习接口的相关知识。
9:接口
在Go语言中,接口是一种类型,它定义了一组方法,这些方法没有实现体。接口定义了一套规范,只要一个类型实现了这些方法,那么它就实现了这个接口。
接口的定义格式如下:
type 接口名 interface {
方法名1(参数列表) 返回值列表
方法名2(参数列表) 返回值列表
...
}
例如,我们可以定义一个Animal接口,并为它添加一个Eat方法:
type Animal interface {
Eat(food string)
}
func main() {
var animal Animal
animal = &Cat{}
animal.Eat("小鱼干") // 输出:猫吃小鱼干。
animal = &Dog{}
animal.Eat("骨头") // 输出:狗啃骨头。
}
type Cat struct{}
func (c *Cat) Eat(food string) {
fmt.Printf("猫吃%s。\n", food)
}
type Dog struct{}
func (d *Dog) Eat(food string) {
fmt.Printf("狗啃%s。\n", food)
}
上面的例子中,Animal接口定义了一个Eat方法,然后我们分别为Cat和Dog类型实现了这个方法。在main函数中,我们定义了一个Animal类型的变量animal,并分别将Cat和Dog类型的实例赋值给它。因为它们都实现了Animal接口定义的Eat方法,所以我们可以通过animal变量调用Eat方法。
除了接口类型的变量可以指向实现了该接口的类型之外,接口还可以嵌套:
type Programmer interface {
WriteHelloWorld()
}
type Person interface {
SayHello()
Programmer
}
type Student struct{}
func (s *Student) SayHello() {
fmt.Println("你好,我是学生。")
}
func (s *Student) WriteHelloWorld() {
fmt.Println("学生写下了Hello World。")
}
func main() {
var person Person
person = &Student{}
person.SayHello() // 输出:你好,我是学生。
person.WriteHelloWorld() // 输出:学生写下了Hello World。
}
上面的例子中,Programmer接口定义了一个WriteHelloWorld方法,Person接口嵌套了Programmer接口和SayHello方法,然后我们为Student类型分别实现了这两个接口的方法,并将它赋值给Person类型的变量。最后我们通过person变量分别调用了SayHello和WriteHelloWorld方法。
接口是Go语言中非常重要的概念,掌握接口的使用方法可以使我们的代码更加灵活和可复用。
10:并发
并发是指同时执行多个独立的任务,而并行是指同时执行多个相关的任务。在Go语言中,可以使用goroutine和channel实现并发。
goroutine是一种轻量级的线程,可以在Go语言的运行时环境中创建。使用goroutine的好处在于可以在程序中并发执行多个函数,而不需要显式地创建线程或管理线程的生命周期。
下面是一个使用goroutine实现并发的例子:
func main() {
go printNum(1)
go printNum(2)
time.Sleep(time.Second) // 等待1秒钟
}
func printNum(num int) {
for i := 0; i < 5; i++ {
fmt.Printf("%d ", num)
}
}
上面的例子中,我们定义了一个printNum函数,用于打印数字。然后在main函数中,我们使用go关键字创建了两个goroutine,分别执行printNum函数打印数字。为了保证goroutine执行完毕,我们使用time.Sleep函数等待1秒钟。
channel是一种用于在goroutine之间进行通信的数据结构。在Go语言中,使用make函数创建channel,并使用<-符号进行数据的发送和接收。
下面是一个使用channel实现并发的例子:
func main() {
ch := make(chan int)
go sendNum(ch)
go receiveNum(ch)
time.Sleep(time.Second)
}
func sendNum(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
}
func receiveNum(ch chan int) {
for i := 0; i < 5; i++ {
num := <-ch
fmt.Printf("%d ", num)
}
}
上面的例子中,我们定义了一个sendNum函数和一个receiveNum函数,用于向channel发送数据和从channel接收数据。然后在main函数中,我们创建了一个channel,分别使用go关键字调用sendNum和receiveNum函数,并使用time.Sleep函数等待1秒钟。
11:错误处理
在Go语言中,错误处理是一个很重要的概念。通常情况下,函数的返回值中会包含一个error类型的值,用于表示函数执行过程中是否发生了错误。
下面是一个示例代码:
func divide(dividend int, divisor int) (int, error) {
if divisor == 0 {
return 0, errors.New("division by zero")
}
return dividend / divisor, nil
}
上面的例子中,我们定义了一个divide函数,用于实现两个整数的除法操作。在函数中,如果除数为0,就会返回一个包含错误信息的error类型的值,否则返回除法的结果和一个空的error值。
在调用divide函数时,可以使用if语句判断函数返回值中的error类型的值是否为nil来判断函数是否执行成功。
result, err := divide(10, 0)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(result)
}
上面的代码中,我们先调用divide函数进行除法运算,然后判断函数的返回值中的error类型的值是否为nil。如果不为nil,就说明函数执行失败,打印错误信息。否则打印除法的结果。
除了使用errors.New函数创建错误信息之外,Go语言还提供了fmt.Errorf函数来创建带有格式化字符串的错误信息,使用方法与fmt.Sprintf类似。
12:并发编程
Go语言中的并发编程是一大特色,可以利用Go语言提供的goroutine和channel机制实现并发执行的程序。
goroutine是一种轻量级的线程,可以在Go语言程序中创建成千上万个goroutine,每个goroutine都会被Go语言的调度器进行调度。
channel是一种通信机制,用于实现不同goroutine之间的数据传递和同步。通常情况下,channel是有类型的,用于传递特定类型的数据。
下面是一个示例代码,使用goroutine和channel实现了一个简单的并发程序。
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
}
上面的代码中,我们首先创建了一个整型的channel,然后在一个匿名函数中使用for循环向channel中发送整数数据,并在循环结束后关闭了channel。
在主函数中,使用range关键字从channel中接收数据并打印出来。由于在匿名函数中关闭了channel,所以当所有数据都被接收后,range循环会自动退出。
上面的代码中,我们使用了一个goroutine和一个channel来实现并发执行的程序,这是Go语言中常用的并发编程方式之一。
13:反射
Go语言中的反射机制可以在程序运行时动态获取类型信息和对变量进行操作,是一种非常强大的编程技术。
反射机制通常使用reflect包来实现,可以通过调用reflect包中的函数来获取类型信息和进行反射操作。
下面是一个示例代码,使用反射机制获取一个结构体中的字段信息。
type User struct {
Name string
Age int
}
func main() {
u := User{"Tom", 20}
v := reflect.ValueOf(u)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fmt.Printf("field %d: %s = %v\n", i, field.Type(), field.Interface())
}
}
上面的代码中,我们定义了一个名为User的结构体类型,并创建了一个实例u。然后使用reflect.ValueOf函数获取实例u的反射对象,并使用反射对象中的NumField和Field方法获取结构体中的字段信息。
在循环中,我们使用反射对象中的Type和Interface方法获取字段的类型信息和实际值,并将其打印出来。
上面的代码中,我们使用了反射机制来获取结构体中的字段信息,这是Go语言中常用的反射操作之一。
14:接口
接口是Go语言中的一个重要概念,可以用于定义一组方法的集合,从而实现多态性编程。
在Go语言中,接口是一种抽象类型,不包含任何实现代码,只定义了一组方法。任何实现了这组方法的类型都可以被看作是实现了该接口。
下面是一个示例代码,定义了一个名为Writer的接口类型,并实现了一个名为FileWriter的类型来实现该接口。
type Writer interface {
Write(data []byte) (int, error)
}
type FileWriter struct {
FileName string
}
func (fw *FileWriter) Write(data []byte) (int, error) {
file, err := os.OpenFile(fw.FileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return 0, err
}
defer file.Close()
return file.Write(data)
}
func main() {
writer := &FileWriter{FileName: "output.txt"}
data := []byte("Hello, world!")
writer.Write(data)
}
上面的代码中,我们首先定义了一个名为Writer的接口类型,并定义了一个Write方法。然后定义了一个名为FileWriter的类型,并实现了Write方法来满足Writer接口的要求。
在main函数中,我们创建了一个名为writer的FileWriter类型的实例,并调用Write方法来向文件中写入数据。
上面的代码中,我们使用了接口来定义一组方法的集合,并通过实现接口的方式来实现多态性编程,这是Go语言中常用的编程方式之一。
15:Goroutine
Goroutine是Go语言中的一种轻量级线程,可以用于并发执行多个任务。
在Go语言中,Goroutine是通过在函数或方法调用前加上go关键字来创建的,可以使用channel来实现Goroutine之间的通信和同步。
下面是一个示例代码,使用Goroutine来并发执行多个任务。
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d started job %d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
for r := 1; r <= 9; r++ {
<-results
}
}
上面的代码中,我们定义了一个名为worker的函数来模拟一个任务执行过程,使用Goroutine来并发执行多个任务。
在main函数中,我们创建了两个channel,一个用于传递任务,一个用于传递结果。然后使用Goroutine创建了三个worker,并将任务分配给它们来执行。最后,我们从结果channel中读取数据,以确保所有任务都已完成。
上面的代码中,我们使用了Goroutine来实现并发执行多个任务,这是Go语言中常用的编程方式之一。
16:defer语句
defer语句用于延迟执行一个函数或方法,该语句会在函数或方法执行完毕后执行。
defer语句可以用于释放资源、关闭文件、打印日志等操作。defer语句的执行顺序是“后进先出”,也就是说,最后一个defer语句会最先执行,而第一个defer语句会最后执行。
下面是一个示例代码,演示了defer语句的用法。
func main() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3")
fmt.Println("main")
}
上面的代码中,我们在main函数中使用了三个defer语句来延迟执行打印语句。当程序运行时,首先会执行fmt.Println(“main”)语句,然后依次执行三个defer语句,输出结果如下:
main
defer 3
defer 2
defer 1
从结果可以看出,defer语句的执行顺序是“后进先出”,最后一个defer语句会最先执行,而第一个defer语句会最后执行。
在实际编程中,我们可以使用defer语句来释放资源、关闭文件、打印日志等操作,这样可以确保这些操作在函数或方法执行完毕后一定会被执行,避免资源泄露等问题的发生。
17:panic和recover
Go语言中的panic和recover是用于处理程序中的错误和异常的机制。
当程序出现不可恢复的错误时,可以使用panic函数引发一个panic错误。当函数中出现panic错误时,该函数会立即停止执行,并且会依次执行该函数中所有defer语句。然后,程序会向上返回,执行调用该函数的函数中的defer语句,直到栈中所有函数的defer语句都执行完毕,然后程序才会退出。
recover函数用于捕获panic错误,并在程序中进行处理。如果在defer语句中使用了recover函数,并且该defer语句是在引发panic错误的函数中执行的,那么程序会在执行到该defer语句时恢复正常,并返回一个非nil的error值,表示程序已经恢复正常。
下面是一个示例代码,演示了panic和recover的用法。
func test() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
fmt.Println("start")
panic("something wrong")
fmt.Println("end")
}
func main() {
test()
fmt.Println("continue")
}
上面的代码中,我们定义了一个test函数,该函数中使用了defer语句来捕获panic错误,并在程序中进行处理。当test函数执行时,会依次执行打印语句、引发panic错误和defer语句。由于引发了panic错误,所以在执行到引发panic错误的语句后,程序会立即停止执行,并且执行test函数中的defer语句。在defer语句中使用了recover函数来捕获panic错误,并打印出错误信息。最后,程序会恢复正常,并返回一个非nil的error值。在main函数中,我们调用了test函数,并在函数执行完毕后继续执行打印语句,输出结果如下:
start
something wrong
continue
从结果可以看出,当程序引发panic错误时,程序会立即停止执行,并依次执行所有defer语句。在defer语句中使用recover函数可以捕获panic错误,并在程序中进行处理,避免程序意外退出。
18:并发和并行
Go语言是一门天生支持并发编程的语言,具有轻量级的线程,称为goroutine。与传统的线程相比,goroutine更加轻量级和高效,因为它们可以在单个线程中执行,而不需要线程上下文切换的开销。这使得Go语言非常适合编写高并发和高性能的应用程序。
在Go语言中,goroutine的调度是由Go运行时系统负责的,开发者无需手动创建线程或管理线程池。因此,编写并发程序非常容易,只需使用go关键字启动一个goroutine即可。
并发和并行是两个相似但不同的概念。并发是指多个任务交替执行,因此看起来好像它们是同时执行的。这是通过在单个处理器上快速切换任务的方式实现的。另一方面,并行是指多个任务同时执行,因此它们确实是同时执行的。这是通过将任务分配给多个处理器来实现的。
在Go语言中,可以使用goroutine来实现并发。由于goroutine是轻量级的,因此可以同时启动数千个goroutine来执行任务,从而实现高并发。此外,Go语言还提供了基于channel的通信机制,可以在goroutine之间进行通信和同步,避免了传统线程之间的锁竞争等问题。
下面是一个简单的示例代码,演示了如何使用goroutine实现并发。
func main() {
for i := 0; i < 10; i++ {
go func(x int) {
fmt.Println(x)
}(i)
}
fmt.Println("finish")
}
上面的代码中,我们使用了for循环启动了10个goroutine,并在每个goroutine中打印了一个数字。由于goroutine的执行是异步的,因此在执行for循环之后,程序会立即输出"finish",而打印数字的操作将在后台继续进行,输出结果如下:
finish
0
1
2
3
4
5
6
7
8
9
从结果可以看出,goroutine的启动是非常快速的,可以同时启动多个goroutine来实现并发执行。
19:Go语言中的内存管理
Go语言具有一种称为垃圾收集器的内存管理机制,它会自动跟踪和管理程序中分配的内存,并自动释放不再使用的内存。这意味着开发者不需要手动管理内存,可以更专注于程序的逻辑实现。
Go语言中的垃圾收集器使用的是标记-清除算法。该算法通过扫描堆中的对象,并标记那些仍然被引用的对象,然后将未标记的对象释放。这种算法具有很好的效率和灵活性,适用于Go语言这种具有高并发和轻量级goroutine的语言。
在Go语言中,开发者可以使用内置的runtime
包来了解和调整垃圾收集器的行为。例如,可以使用runtime.GOMAXPROCS()
函数来设置可用于并发执行goroutine的处理器数量。此外,开发者还可以使用runtime.ReadMemStats()
函数来获取有关程序当前内存使用情况的详细信息,如总分配的内存量、当前使用的内存量和未释放的内存量等。
需要注意的是,尽管Go语言具有自动内存管理机制,但仍然需要开发者编写高效的代码,以减少不必要的内存分配和使用。例如,可以使用指针来避免复制大型数据结构,或使用内置的sync.Pool
来重用常用的对象,以减少垃圾收集器的压力。
下面是一个简单的示例代码,演示了如何使用runtime
包来获取程序的内存使用情况。
import (
"fmt"
"runtime"
)
func main() {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
fmt.Printf("Allocated memory: %d bytes\n", memStats.Alloc)
fmt.Printf("Total memory: %d bytes\n", memStats.Sys)
fmt.Printf("Heap memory: %d bytes\n", memStats.HeapAlloc)
fmt.Printf("Heap sys memory: %d bytes\n", memStats.HeapSys)
}
上面的代码中,我们使用了runtime
包中的MemStats
类型和ReadMemStats()
函数来获取有关程序当前内存使用情况的详细信息,并将其打印出来。输出结果如下:
Allocated memory: 12016 bytes
Total memory: 4016640 bytes
Heap memory: 12016 bytes
Heap sys memory: 655360 bytes
从结果可以看出,程序总共分配了12016字节的内存,总共使用了4016640字节的内存,其中堆内存和堆系统内存分别为12016字节和655360字节。
Go语言的控制结构的,包括if语句、switch语句、for循环、跳转语句等。以下是关于这些内容的总结:
- if语句
if语句用于判断某个条件是否成立,如果条件成立则执行if语句块内的代码,否则不执行。语法结构如下:
if condition {
// if 语句块
} else if condition2 {
// else if 语句块
} else {
// else 语句块
}
- switch语句
switch语句用于根据不同的条件执行不同的代码块。语法结构如下:
switch expression {
case value1:
// case1 语句块
case value2:
// case2 语句块
...
default:
// default 语句块
}
- for循环
for循环用于重复执行一段代码块,直到满足某个条件才停止循环。语法结构如下:
for initialization; condition; increment {
// 循环体
}
其中,initialization表示循环变量的初始化语句,condition表示循环条件,increment表示循环变量的增量语句。
- 跳转语句
跳转语句用于控制程序执行的流程,包括break、continue和goto语句。
- break语句用于跳出循环,终止循环体的执行。
- continue语句用于跳过本次循环,直接进行下一次循环。
- goto语句用于无条件地跳转到指定标签处执行代码。
注意:尽量避免使用goto语句,因为它容易导致代码结构复杂、难以维护。