Go语言是门面向对象的编程语言 面向对象:化繁为简, 能不自己干自己就不干,关注的是我应该让谁来做?
变量
1 2 3 4 var 变量名 变量类型var name string var age int
1 2 3 4 5 var 变量名 类型 = 表达式var a int = 10 var name string = "yy" var x, y, z int = 1 , 2 , 3
1 2 3 4 5 6 7 8 9 var a = 42 var b = 3.14 var c = "hello" var a
1 2 3 4 5 c := 3.14 d := 42 e := 3.14 f := true
1 2 3 4 5 6 7 var ( a string b int c bool d float32 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func main () { var _ = "Hello, World!" fmt.Println(_) var _, _ = 1 , 2 fmt.Println(_, _) }
变量的零值
整型 :
int
类型的零值是 0
。
uint
类型的零值是 0
。
浮点型 :
float32
类型的零值是 0.0
。
float64
类型的零值是 0.0
。
布尔型 :
字符串 :
指针 :
切片 :
映射 :
通道 :
结构体 :
结构体类型的零值是零值是结构体的零初始化,例如 struct{}
。
接口 :
常量 常量用于存储固定不变的值,其值在程序运行期间不能改变。使用 const 关键字声明常量。
声明了pi
和e
这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了
1 2 const pi = 3.1415 const e = 2.7182
枚举常量 1 2 3 4 5 const ( A = iota B = iota C = iota )
1 2 3 4 5 6 const ( n1 = iota n2 _ n4 )
1 2 3 4 5 6 7 const ( n1 = iota n2 = 100 n3 = iota n4 ) const n5 = iota
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import "fmt" func main () { const ( a = iota // 这是第一次使用 iota,所以 a = 0 b // 这是第二次使用 iota,所以 b = 1 c // 这是第三次使用 iota,所以 c = 2 d = "ha" // 这里没有使用 iota,所以 iota 的计数器不会增加 e // 这是第四次使用 iota,所以 e = 0(因为上一次赋值重置了 iota) f = 100 // 这里没有使用 iota,所以 iota 的计数器不会增加 g // 这是第五次使用 iota,所以 g = 1(继续上一次的计数) h = iota // 这里显式地使用 iota,所以 h = 7(因为这是新的一轮,从0开始,然后增加7次) i // 这是第六次使用 iota,所以 i = 8 ) fmt.Println(a, b, c, d, e, f, g, h, i) }
常量和变量在内存区域的存储 Go 内存区域划分 Go 语言中,内存通常分为三大区域:
全局静态区:存储全局变量和常量。这些变量和常量在程序启动时分配内存,并在程序结束时释放。
就像是家里的储藏室,存放不常变动的东西。
栈与堆的差异
变量的内存存储
全局变量 :全局变量声明在函数外,属于静态存储区,存储在全局静态区中。这些变量在程序启动时被分配,程序结束时被释放。全局变量的生命周期与程序的生命周期相同,程序一启动就会分配存储空间,直到程序结束。
局部变量 :局部变量是在函数内部声明的,它们通常存储在栈上(如果不逃逸)。当函数调用时,栈帧会为局部变量分配内存;当函数返回时,这些栈内存会自动释放。如果局部变量逃逸到堆中,那么数据会存储在堆上。作用域:从定义哪一行开始直到与其所在的代码块结束
指针变量 :指针变量存储在栈上或堆上,但它们指向的数据可以位于堆中。如果变量逃逸到堆中,那么数据会存储在堆上。
常量的内存存储
编译时常量 :常量使用 const
关键字声明,它们通常在编译时确定,并且不会在运行时分配实际的存储空间。常量在内存中的表现方式与字面量类似,在很多情况下,编译器会直接将常量的值内联到使用它的地方。由于常量的值在编译时确定,因此它们不会像变量一样占用运行时内存。
只读常量 :对于某些复杂常量(例如字符串),它们会存储在只读的内存区,通常位于全局静态区中。尽管这些常量不会在运行时更改,它们可能仍然占用一些内存资源。
内存逃逸分析 在Go语言中,逃逸分析(escape analysis)是一种编译期优化技术,用于确定局部变量的存储位置。如果变量只在当前函数内部使用,并且没有被传递到其他函数或goroutine,那么这个变量通常存储在栈(stack)上。如果变量的生命周期超出了当前函数,比如被返回或传递给了其他goroutine,那么这个变量就会“逃逸”到堆(heap)上。
让我们用大白话来解释一下这个过程:
逃逸分析是什么? 逃逸分析就像是侦探工作,编译器像侦探一样,追踪每个局部变量的去向。如果变量只在当前函数内部使用,那么它就老老实实地呆在栈上。但如果变量被带出了当前函数,比如被函数返回,或者被发送到另一个goroutine,那么它就需要一个更长久的家——堆。
为什么变量会逃逸到堆上?
变量被返回 :
如果一个变量的地址被返回,那么它可能被其他函数长时间持有,所以它需要存储在堆上,以免在函数返回后被销毁。
变量被传递给goroutine :
如果一个变量被传递给一个新的goroutine,那么它的生命周期就不再局限于当前函数,它需要在堆上分配内存,以确保在goroutine执行期间变量仍然有效。
什么时候会发生 :
如果局部变量的生命周期超出了它所在的函数,比如被函数返回或者被传递给了其他的goroutine,那么它就需要一个更长久的存储位置,这时候它就会“逃逸”到堆上。
堆的作用 :
堆就像是家里的储藏室,用来存放那些需要长时间保存的东西。在Go语言中,当局部变量逃逸到堆上时,就意味着它们被分配在堆上,可以被程序的其他部分长期访问。
1 2 3 4 func escapeExample () *int { var x int = 42 return &x }
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 package mainimport "fmt" func main () { var intV int var floatV float32 var boolV bool var stringV string var pointerV *int var funcV func (int , int ) int var interfaceV interface {} var sliceV []int var channelV chan int var mapV map [string ]string var errorV error fmt.Println("int = " , intV) fmt.Println("float = " , floatV) fmt.Println("bool = " , boolV) fmt.Println("string = " , stringV) fmt.Println("pointer = " , pointerV) fmt.Println("func = " , funcV) fmt.Println("interface = " , interfaceV) fmt.Println("slice = " , sliceV) fmt.Println("slice = " , sliceV == nil ) fmt.Println("channel = " , channelV) fmt.Println("map = " , mapV) fmt.Println("map = " , mapV == nil ) fmt.Println("error = " , errorV) var arraryV [3 ]int type Person struct { name string age int } var structV Person fmt.Println("arrary = " , arraryV) fmt.Println("struct = " , structV) }
复合数据类型 数组(Array) 数组是具有固定长度的同类型元素集合。一旦定义,数组的长度就固定,不能改变。
1 2 3 var arr [5 ]int = [5 ]int {1 , 2 , 3 , 4 , 5 } balance := [5 ]float32 {1000.0 , 2.0 , 3.4 , 7.0 , 50.0 } fmt.Println(len (arr))
如果数组长度不确定,可以使用 … 代替数组的长度,编译器会根据元素个数自行推断数组的长度:
1 2 var balance = [...]float32 {1000.0 , 2.0 , 3.4 , 7.0 , 50.0 }balance := [...]float32 {1000.0 , 2.0 , 3.4 , 7.0 , 50.0 }
遍历数组
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { arr := [...]int {1 , 3 , 5 } for i:=0 ; i<len (arr); i++{ fmt.Println(i, arr[i]) } for i, v := range arr{ fmt.Println(i, v) } }
二维数组
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { arr := [2 ][3 ]int { {1 , 2 , 3 }, {4 , 5 , 6 }, } fmt.Println(arr) }
1 2 3 4 5 6 7 8 9 10 package main import "fmt" func main () { arr := [...][3 ]int { {1 , 2 , 3 }, {4 , 5 , 6 }, } fmt.Println(arr) }
切片(Slice)
切片是基于数组的动态数组,长度可以变化,切片不需要指定长度,可以根据需求动态增减长度。
切片是引用类型,它是对底层数组的引用,因此修改切片会影响底层数组的内容。
切片由三个部分组成:指向底层数组的指针、切片的长度和切片的容量。
1 2 3 4 var s []int s := []int {1 , 2 , 3 , 4 , 5 } s[0 ] = 10 fmt.Println(s[0 ])
1 2 var slice []int = []int {1 , 2 , 3 }slice = append (slice, 4 )
1 2 3 s1 := []int {1 , 2 , 3 } s2 := make ([]int , len (s1)) copy (s2, s1)
make函数创建 make(类型, 长度, 容量)
内部会先创建一个数组, 然后让切片指向数组
如果没有指定容量,那么容量和长度一样
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { var sce = make ([]int , 3 , 5 ) fmt.Println(sce) fmt.Println(len (sce)) fmt.Println(cap (sce)) }
1 2 3 4 5 6 var slice4 []int = []int {1 ,2 ,3 ,4 ,5 }var slice5 = make ([]int ,10 )copy (slice5,slice4) fmt.Println(slice4) fmt.Println(slice5)
1 2 3 4 5 6 type slice struct { array unsafe.Pointer len int cap int }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ("fmt" )func main () { var intArr [5 ]int = [...]int {11 ,22 ,33 ,44 ,55 } slice := intArr[1 :3 ] fmt.Println("intarr=" ,intArr) fmt.Println("intarr的容量是 " ,len (inArr)) fmt.Println("slice 的元素是 " ,slice) fmt.Println("slice 的容量是" ,cap (slice)) fmt.Println("slice 的元素个数为" ,len (slice)) }
1 2 3 4 s := []int {1 , 2 , 3 , 4 , 5 } s1 := s[1 :4 ] s2 := s[2 :]
1 2 3 4 5 6 7 8 func modifySlice (s []int ) { s[0 ] = 100 } s := []int {1 , 2 , 3 } modifySlice(s) fmt.Println(s)
字典(Map) 字典是一种键值对数据结构,用于高效地存储和检索元素。
1 2 var m map [string ]int = map [string ]int {"foo" : 1 , "bar" : 2 }
map的增删改查
当 map
中没有指定的键时,就会自动增加键值对。
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { var dict = make (map [string ]string ) fmt.Println("增加前:" , dict) dict["name" ] = "ln" fmt.Println("增加后:" , dict) }
当 map
中有指定的键时,就会自动修改键对应的值。
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { var dict = map [string ]string {"name" : "ln" , "age" : "3" , "gender" : "male" } fmt.Println("修改前:" , dict) dict["name" ] = "zs" fmt.Println("修改后:" , dict) }
可以通过 Go 语言内置的 delete
函数删除指定键的元素。
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func main () { var dict = map [string ]string {"name" : "ln" , "age" : "3" , "gender" : "male" } fmt.Println("删除前:" , dict) delete (dict, "name" ) fmt.Println("删除后:" , dict) }
通过 ok-idiom
模式判断指定键值是否存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { var dict = map [string ]string {"name" : "ln" , "age" : "3" , "gender" : "male" } if value, ok := dict["age" ]; ok{ fmt.Println("有age这个key,值为" , value) fmt.Println("没有age这个key,值为" , !ok) }
遍历 map
时,map
中存储的数据是无序的,所以多次输出的顺序可能不同。
1 2 3 4 var dict = map [string ]string {"name" : "ln" , "age" : "3" , "gender" : "male" }for key, value := range dict { fmt.Println(key, value) }
结构体(Struct) 结构体是一种复合数据类型,用于将不同类型的数据组合在一起。
1 2 3 4 5 type Person struct { Name string Age int }
1 2 3 4 5 6 p := Person{ FirstName: "John" , LastName: "Doe" , Age: 30 , } fmt.Println(p.FirstName)
指针(Pointer) 指针存储变量的内存地址。Go 不支持指针运算,但可以通过指针来改变原变量的值。
1 2 3 var x int = 10 var p *int = &x *p = 20
特殊数据类型 接口(Interface) 接口定义了一组方法,而不实现这些方法。实现接口的具体类型必须实现接口中定义的所有方法。
1 2 3 type Speaker interface { Speak() }
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 package mainimport "fmt" type usber interface { start() stop() } type Computer struct { name string model string } func (cm Computer) start() { fmt.Println("启动电脑" ) } func (cm Computer) stop() { fmt.Println("关闭电脑" ) } type Phone struct { name string model string } func (p Phone) start() { fmt.Println("启动手机" ) } func (p Phone) stop() { fmt.Println("关闭手机" ) } func working (u usber) { u.start() u.stop() } func main () { cm := Computer{"戴尔" , "F1234" } working(cm) p := Phone{"华为" , "M10" } working(p) }
在Go语言中,接口是一种类型,它定义了一组方法。如果一个类型实现了接口中声明的所有方法,我们就说这个类型实现了这个接口。多态 就是通过接口来实现的,它允许你使用接口类型的变量来引用任何实现了该接口的具体类型。
接口定义行为 :接口是方法的集合,定义了行为的抽象。
类型自动实现接口 :Go 没有显式的 implements
关键字,只要类型实现了接口中的方法,它就自动被视为该接口类型。
多态性 :通过接口,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 package mainimport ( "fmt" ) type Animal interface { Speak() string } type Dog struct {}type Cat struct {}func (d Dog) Speak() string { return "Woof!" } func (c Cat) Speak() string { return "Meow!" } func MakeSound (a Animal) { fmt.Println(a.Speak()) } func main () { dog := Dog{} cat := Cat{} MakeSound(dog) MakeSound(cat) }
进阶示例:增加更多的实现类型
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 Cow struct {}type Duck struct {}func (c Cow) Speak() string { return "Moo!" } func (d Duck) Speak() string { return "Quack!" } func main () { animals := []Animal{Dog{}, Cat{}, Cow{}, Duck{}} for _, animal := range animals { MakeSound(animal) } }
面向对象设计思想与接口的关系
在面向对象设计中,接口扮演了模块间的“通信契约”,它帮助我们分离具体实现和高层逻辑:
抽象和封装 :接口提供了一种抽象,定义了一组方法,而不关心方法的具体实现。每个实现接口的类型可以有不同的内部逻辑,但对外表现一致。
多态性 :接口允许我们在不同类型之间实现多态,即用同一个接口类型的变量来操作不同的具体类型。在本例中,Animal
接口实现了多态性,使得 Dog
、Cat
、Cow
、Duck
都可以作为 Animal
来使用。
解耦和扩展性 :接口使代码更加解耦。调用者只需要知道接口,而不需要关心具体实现,符合“依赖于抽象而不依赖于具体”的设计原则。这种模式特别适合需要不断扩展的新类型需求。
类型断言
用于检查接口变量(interface)中实际存储的值的类型,并在检查的同时进行类型转换。
检查接口变量中存储的具体类型 :当你不确定接口变量中存储的是哪个具体类型时,可以使用类型断言来检查。
从接口变量中提取具体类型的值 :接口变量只能调用接口中定义的方法,如果你需要调用具体类型的方法,可以使用类型断言来提取具体的值。
1 2 双值形式:t, ok := i.(Type) 单值形式:t := i.(Type),如果类型不匹配,会直接引发 panic
1 2 3 4 5 i 是接口变量。 Type 是要断言的具体类型。 t 是断言成功后的具体类型的变量。 ok 是一个布尔值,表示断言是否成功。 如果 i 的实际类型与 Type 相同,那么 t 会是 Type 类型的变量,ok 为 true 。如果类型不匹配,则 ok 为 false ,t 的值为零值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { var a Animal = Dog{} dog, ok := a.(Dog) if ok { fmt.Println("a 是 Dog 类型:" , dog.Speak()) } else { fmt.Println("a 不是 Dog 类型" ) } cat := a.(Cat) fmt.Println("a 是 Cat 类型:" , cat.Speak()) }
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 mainimport ( "fmt" ) type Animal interface { Speak() string } type Dog struct {}func (d Dog) Speak() string { return "Woof!" } type Cat struct {}func (c Cat) Speak() string { return "Meow!" } func main () { var a Animal a = Dog{} dog, ok := a.(Dog) if ok { fmt.Println("a 是 Dog 类型,叫声是:" , dog.Speak()) } else { fmt.Println("a 不是 Dog 类型" ) } cat, ok := a.(Cat) if ok { fmt.Println("a 是 Cat 类型,叫声是:" , cat.Speak()) } else { fmt.Println("a 不是 Cat 类型" ) } }
1 2 a 是 Dog 类型,叫声是: Woof! a 不是 Cat 类型
type switch
:简化多种类型断言
1 2 3 4 5 6 7 8 9 10 11 switch v := x.(type ) {case T1: case T2: case T3: default : }
1 2 3 x 是一个接口类型的变量。 v := x.(type ) 是 type switch 的声明部分,v 是一个新的变量,它将存储 x 中实际存储的值。 T1、T2、T3 等是你要检查的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func DescribeAnimal (a Animal) { switch v := a.(type ) { case Dog: fmt.Println("这是一只狗,它的叫声是:" , v.Speak()) case Cat: fmt.Println("这是一只猫,它的叫声是:" , v.Speak()) default : fmt.Println("未知类型的动物" ) } } func main () { DescribeAnimal(Dog{}) DescribeAnimal(Cat{}) }
函数(Function) 函数在 Go 中也是一种数据类型,可以作为参数或返回值传递给其他函数。
1 2 3 func add (a, b int ) int { return a + b }
通道(Channel) 通道是 Go 语言中的一种用于并发的类型,用于在 goroutine 之间传递数据。
你在一家餐厅里,餐厅里有厨房和餐桌。厨师们在厨房里做好菜,然后需要把菜送到顾客的餐桌上。但是,我们不能让厨师直接冲到顾客面前送菜,这样会乱套的。所以,餐厅里有一种叫做“传菜口”的东西,厨师把菜通过传菜口递出来,服务员从传菜口取走菜,然后送到顾客的桌子上。
在Go语言里,通道(Channel)就有点像这个“传菜口”。它是一个让不同的工作线程(我们叫它们“goroutine”)之间传递消息和数据的通道。这些goroutine就像是厨师和服务员,它们不能直接交换东西,而是通过通道来传递。
通道的几个特点:
类型安全 :就像传菜口只能递菜一样,Go语言中的通道也只能传递一种类型的数据。
同步 :通道保证了数据的发送和接收是同步的。就像厨师把菜放进传菜口,必须有服务员来取走,才能继续做菜。
缓冲 :有些通道可以存一些数据,就像传菜口可以暂时放几盘菜一样。如果通道里已经存满了菜,厨师就得等服务员取走一些,才能放下一盘新菜。
关闭 :就像餐厅打烊后,厨师不会再做菜,通道也可以关闭,表示不会再有新的数据发送进来了。
为什么需要通道:
在Go语言里,我们经常需要同时做几件事情(并发),就像餐厅里同时有几桌顾客点菜一样。通道就是确保这些同时进行的事情能够协调工作,不会乱套的一种方式。它不仅让数据传递变得有序,还能保证数据的安全,因为数据只能在通道里传递,不会被别的程序或者goroutine乱改。
通道有 发送 (send)、接收 (receive)和 关闭 (close)三种操作。
发送和接收都使用<-
符号。
无缓冲的channel 在发送和接收数据时是同步的:
发送操作 :当一个goroutine向无缓冲的channel发送数据时,它必须等待另一个goroutine来接收这个数据,否则发送操作会一直阻塞。
接收操作 :同样地,当一个goroutine从无缓冲的channel接收数据时,它也必须等待另一个goroutine发送数据,否则接收操作会一直阻塞。
同步 :无缓冲的channel确保了发送和接收操作的同步性,即数据的发送和接收是严格交替进行的。
使用场景 :无缓冲的channel常用于需要严格同步操作的场景,比如协调两个goroutine的工作流程。
1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "fmt" )func main () { ch := make (chan int ) go func () { ch <- 42 }() value := <-ch fmt.Println("接收到的值:" , value) }
有缓冲的channel (类比电话留言箱,无需对方立即接听)允许在没有接收者的情况下发送一定数量的数据,这个数量就是channel的缓冲大小:
发送操作 :如果缓冲区未满,发送数据到有缓冲的channel不会阻塞,数据会被放入缓冲区中。只有当缓冲区满了,发送者才会阻塞,直到缓冲区有空间。
接收操作 :如果缓冲区不为空,从有缓冲的channel接收数据不会阻塞,因为可以直接从缓冲区中取出数据。只有当缓冲区空了,接收者才会阻塞,直到有数据发送到channel。
异步 :有缓冲的channel允许一定程度的异步操作,因为数据可以先存储在缓冲区中,不必立即被接收。
使用场景 :有缓冲的channel适用于生产者-消费者问题,其中生产者可以快速地将数据放入缓冲区,而消费者则可以异步地从缓冲区中取出数据
注意:在关闭 channel 后,不要再向其发送数据,这样会引发 panic。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { tasks := make (chan int , 10 ) for i := 0 ; i < 10 ; i++ { fmt.Println("task " , i) tasks <- i } close (tasks) for task := range tasks { fmt.Println("处理任务:" , task) } }
数据类型转换 Go 是静态类型语言,必须显式地进行类型转换,不能像某些动态语言一样自动进行隐式类型转换。
1 2 var a int = 10 var b float64 = float64 (a)
运算符
1 2 3 4 5 6 7 8 9 10 11 12 a := 6 b := 3 fmt.Println(a & b) fmt.Println(a | b) fmt.Println(a ^ b) fmt.Println(a << 1 ) fmt.Println(b >> 1 )
1 2 3 4 5 6 7 8 9 10 11 12 package main import "unsafe" const ( a = "abc" b = len (a) c = unsafe.Sizeof(a)) func main () { println (a, b, c) } abc 3 16
流程控制 常见的分支控制语句包括 if、else、switch、select
for 循环 :Go 中的循环结构主要通过 for
语句实现。Go 没有像其他语言中的 while
和 do-while
循环,而是用 for
语句可以完成所有的循环需求。
1 2 3 4 5 6 for i := 0 ; i < 5 ; i++ { if i == 3 { continue } fmt.Println(i) }
for…range 循环 :for...range
循环用于遍历数组、切片(slice)、map、字符串等集合类型的数据。
1 2 3 4 nums := []int {1 , 2 , 3 , 4 , 5 } for index, value := range nums { fmt.Printf("索引: %d, 值: %d\n" , index, value) }
select 语句 :是 Go 特有的,用于处理多个通道(channel)的通信操作。它类似于 switch
,但每个 case 都必须是一个通道操作。
1 2 3 4 5 6 7 8 9 10 select { case <- channel1: case data := <- channel2: case channel3 <- data: default : }
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 package mainimport ( "fmt" "time" ) func main () { ch1 := make (chan string ) ch2 := make (chan string ) go func () { time.Sleep(2 * time.Second) ch1 <- "from ch1" }() go func () { time.Sleep(1 * time.Second) ch2 <- "from ch2" }() select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) case <-time.After(3 * time.Second): fmt.Println("timeout" ) } }
循环配合 defer 使用 :在循环中,defer
语句会在当前函数返回之前按照后进先出的顺序执行。通常用于在循环中执行一些延迟操作比如文件关闭、资源释放等。
1 2 3 for i := 0 ; i < 3 ; i++ { defer fmt.Println("延迟执行:" , i) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { defer func1() defer func2() defer func3() } func func1 () { fmt.Println("A" ) } func func2 () { fmt.Println("B" ) } func func3 () { fmt.Println("C" ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" func deferFunc () int { fmt.Println("defer func called" ) return 0 } func returnFunc () int { fmt.Println("return func called" ) return 0 } func returnAndDefer () int { defer deferFunc() return returnFunc() } func main () { returnAndDefer() }
1 2 3 return func called defer func called
嵌套循环 :Go 允许在循环内嵌套其他循环,以实现更复杂的逻辑。
1 2 3 4 for i := 1 ; i <= 3 ; i++ { for j := 1 ; j <= 3 ; j++ { fmt.Printf("i: %d, j: %d\n" , i, j) }
在 Go 语言中,字符串是由两部分组成的:
字符串的长度 :告诉我们字符串包含多少个字符。
指向字符串内容的指针 :指向存储字符串实际字符数据的地方。
因此,字符串的总大小计算如下:
8 字节 (指针)+8 字节 (长度)=16 字节