golang基础看这个就够了


前言

本文可以快速了解go基础和上手,适合小白使用,更适合有一定语言基础想快速学习go语言的(因为我就是java转go的哈哈)


一、Go关键字(25个)

1、引导程序整体结构的八个关键字

package       //定义包名的关键字
import        //导入包名的关键字
const         //常量声明的关键字
var           //变量声明的关键字
func          //函数定义的关键字
defer        //延迟执行关键字
go            //并发语法糖关键字
return       //函数返回关键字

2、声明复合数据结构的4个关键字

struct      //定义结构类型的关键字
interface   //定义接口类型的关键字
map         //声明或创建map类型关键字
chan        //声明或创建通道类型关键字

3、控制程序结构的13个关键字

if else                        //if else 语句关键字
for range                     //for循环使用的关键符 
break                         //用于立即终止当前(或内层)循环    
continue                      //用于跳过当前循环的剩余部分,直接进入下一次循环
switch select type case default fallthrough  //switch和select语句使用的关键字
goto                          //goto的跳转语句关键词

defalue: 当switch表达式的值与所有case分支的值都不匹配时,会执default分支中的代码

相关问题

带标签的break(用于跳出多层嵌套循环)

可以在循环语句之前添加一个标签,然后在break语句中使用这个标签,这样就可以跳出指定的循环(不一定是最内层循环)。标签是一个自定义的标识符,后面跟着一个冒号,放置在循环语句的前面。

OuterLoop:
  for i := 0; i < 3; i++ {
     for j := 0; j < 3; j++ {
        if i == 2 && j == 1 {
           break OuterLoop
        }
        fmt.Printf("i: %d, j: %d\n", i, j)
     }
  }

select 关键字的详细解释

select 语句主要用于处理多个通道的 I/O 操作,它可以让程序在多个通道操作上进行选择,哪个通道有数据可读或可写就执行对应的操作。

package main

func main() {
    select {}
}

在上述代码中,select 语句为空,没有任何 case 分支。当程序执行到 select {} 时,会进入永久阻塞状态,程序不会继续向下执行其他代码,除非有外部因素(如程序被强制终止)介入。

如果 case 里涉及的是nil 通道,那么这个 case 会被直接忽略,不会阻塞在这个 case 上等待操作。
不过,函数是否从 default 出口返回得看具体情况:
有 default 分支的情况

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    var nilCh chan int
    select {
    case <-ch:
        fmt.Println("Received from ch")
    case nilCh <- 1: // 这里操作 nil 通道,此 case 会被忽略
        fmt.Println("Sent to nilCh")
    default:
        fmt.Println("Executing default")
    }
}

在上述代码中,因为 nilCh 是 nil 通道,其对应的 case 被忽略,又由于 ch 中也没数据,所以会执行 default 分支,输出 Executing default。

没有 default 分支的情况

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    var nilCh chan int
    select {
    case <-ch:
        fmt.Println("Received from ch")
    case nilCh <- 1: // 这里操作 nil 通道,此 case 会被忽略
        fmt.Println("Sent to nilCh")
    }
    fmt.Println("After select")
}

运行这段代码会发现,程序会阻塞在 select 语句处,不会输出 After select,因为没有 default 分支,且非 nil 通道相关的 case 都没准备好执行,只能一直等待。

select 语句中的每个 case 必须是一个针对通道(channel)的操作,具体包括从通道接收数据(<-channel)或者向通道发送数据(channel <- value)。除了通道操作之外,不允许出现其他普通的表达式或者语句。

switch 语句

switch 语句是一种多分支选择结构,用于根据不同的条件执行不同的代码块。它可以对一个变量或表达式的值进行判断,然后根据其值选择相应的分支执行。switch 语句可以提高代码的可读性和可维护性,避免使用大量的 if-else 语句。

package main

import "fmt"

func main() {
    num := 2
    switch num {
    case 1:
        fmt.Println("Number is 1")
    case 2,3:
        fmt.Println("Number is 2")
    default:
        fmt.Println("Number is neither 1 nor 2")
    }
}

单个case中,可以出现多个结果选项。
只有在case中明确添加fallthrough关键字,才会继续执⾏紧跟的下⼀个case。

fallthrough 关键字的详细解释

fallthrough 关键字仅在 switch 语句中使用。它的主要作用是使程序控制流跳出指定的循环(不一定是最内层循环),而不进行条件检查。

package main

import "fmt"

func main() {
    switch num := 1; num {
    case 1:
       fmt.Println("This is case 1")
       fallthrough
    case 2:
       fmt.Println("This is case 2")
    default:
       fmt.Println("This is default case")
    }
}

代码解释:
switch num := 1; num:这是一个 switch 语句,num 的初始值为 1。
case 1:当 num 的值为 1 时,会执行这个分支,打印 “This is case 1”。
fallthrough:这个关键字使得程序控制流会直接进入下一个 case 分支,即 case 2,而不进行 case 2 的条件检查。
case 2:由于 fallthrough 的存在,程序会继续执行这个分支,打印 “This is case 2”。

range关键字的详细解释

在 Go 语言中,range是一个用于遍历各种数据结构的关键字。它提供了一种简洁的方式来迭代数组、数组指针、切片、字符串、map和channel类型。当使用range时,它会返回每个元素的索引(对于数组、数组指针、切片和字符串)或键值对(对于map),或者从channel接收值。

for-range表达式在遍历开始前就已经决定了循环次数,所以迭代过程中向切片追加元素不会导致循环无休止执行,函数可以正常退出。

for range

for range是将for循环和range关键字结合起来的一种循环形式,专门用于遍历数据结构。它提供了一种简洁的方式来访问数据结构中的元素,而无需手动管理索引和迭代过程。
与传统的for循环相比,for range在遍历数组、切片等数据结构时更加简洁和方便。
例如,

使用传统for循环遍历数组可能需要这样写:
arr := [3]int{1, 2, 3}
for i := 0; i < len(arr); i++ {
   fmt.Println(arr[i])
}
而使用for range可以写成:
for _, v := range arr {
   fmt.Println(v)
}

在使用普通的for循环遍历数据结构时,很容易出现边界条件错误,例如索引越界。for range在遍历数组、切片和字符串时,会自动根据数据结构的长度进行迭代,避免了手动计算索引导致的边界错误。

for循环和for-range循环的区别

当需要精确控制循环次数和迭代过程时,使用 for 循环更合适,当需要遍历集合类型的数据结构(如数组、切片、映射等)并访问其中的元素时,for range循环能使代码更加简洁易读,处理通道数据时,for range循环可以方便地从通道中接收数据,直到通道被关闭。

for-range循环的实际应用

数组
当range用于遍历数组时,它会复制数组。这意味着如果在range循环中修改数组元素,实际上是在修改副本,而不是原始数组。也可以作用于数组指针,效果没有区别。

arr := [3]int{1, 2, 3}
for _, v := range arr {
   v = 0
}
fmt.Println(arr)

输出结果仍然是[1 2 3],因为range复制了数组,在循环中修改的是副本。
range作用于数组时,从下标0开始依次遍历数组元素,返回元素的下标和元素值。

如果要修改数组中的元素,需要通过索引来进行操作,或者使用指针类型的数组。

切片
range作用于切片,也可以用于指向切片的指针,与数组类似,返回元素的下标和元素值。举例如下:

func RangeSlice()(
s :=[]int{1,2,3}
for i, v := range s {
fmt.Printf("index:%d value:%d\n", i, v)
}
函数输出:
index: 0 value:1 
index: l value:2 
index: 2 value:3

package main
import "fmt"
func main() {
    // 定义一个切片
    numbers := []int{1, 2, 3, 4, 5}
    // 定义一个指向切片的指针
    ptr := &numbers
    // 使用 for range 遍历指向切片的指针
    for index, value := range *ptr {
        fmt.Printf("索引 %d 对应的值是 %d\n", index, value)
    }
}

使用for range语句修改切片元素的情况需要分具体场景来讨论
通过 for range 循环借助索引能修改切片或数组的元素值,
当切片或数组元素是指针类型时,在循环内修改指针指向的值同样能影响切片元素所指向的内容
特殊的地方在于切片长度可变:与数组固定长度不同,切片长度可以动态变化,在 for range 循环中如果涉及到对切片进行添加、删除等改变长度的操作要格外小心,可能会出现不符合预期的情况(比如循环次数可能受影响等)

字符串
range 作用于 string时,仍然返回元素的下标和元素值,但由于string底层使用Unicode 编码存储字符,字符可能占用1~4个字节,所以下标可能是不连续的,并且元素值是该字符对应的 Unicode编码的首个字节的值。
对于纯 ASCII码中的字母来说,每个字符的Unicode编码仍占用1个字节:

func RangeString(){
s:="Hello"
for i,v := range s{
fmt,Printf("index:%d,value:%c\n",i,v)
}
函数输出:
index: 0,value:H
index: 1,value:e
index: 2,value:l
index: 3,value:l
index: 4,value:o

对于中文汉字来说,每个字符会占用多个字节,此时元素的下标就不是连续的:

func RangeStringUniCode(){
s:="中国"
for i,v := range s{
fmt.Printf("index:%d,value:%c\n",i,v)
}
函数输出:
index: 0, value:中 index: 3,value:

另外需要注意的是,range的第二个返回值的类型为rune类型,它仅代表Unicode编码的1个字节:

type rune =int32

由于字符串在 Go 语言中是不可变的,所以==使用for range遍历字符串时,不能直接修改字符串中的字符。==不过,可以将字符串中的字符复制到一个可变的数据结构(如切片)中进行修改。

str := "abc"
charSlice := []rune(str)
for i, char := range charSlice {
   charSlice[i] = char + 1     //rang切片时,使用索引,才可以修改成功
}
newStr := string(charSlice)
fmt.Println(newStr)
这里经过测试发现, char + 1这种写法只能用于ASCII码,如果是汉字,会变成其他汉字

map
range 作用于map时,返回每个元素的key和value:

func RangeMap(){
m:= map[string]string("animal": "monkey", "fruit": "apple"} 
for k, v:= range m {
fmt.Printf("key: %s,value: %s\n",k,v)
}
函数可能输出:
key: animal,value:monkey key: fruit, value: apple
也可能输出:
key: fruit, value: apple key: animal, value:monkey

由于map的数据结构本身没有顺序的概念,它仅存储key-value对,所以 range分别返回 key和 value。
在 for range 循环中,k 是映射的键,v 是映射的值。可以通过 m[k] 直接修改映射中键对应的值
另外,如果遍历过程中增加或删除元素,则range行为是不确定的,删除的元素不可能被遍历到,新加的元素可能遍历不到

channel
range用于channel时,会一直从channel接收值,直到channel被关闭。

ch := make(chan int)
go func() {
   for i := 0; i < 3; i++ {
      ch <- i
   }
   close(ch)
}()
for v := range ch {
   fmt.Printf("从channel接收的值: %d\n", v)
}
这个例子中,range会阻塞等待channel中的值,当channel关闭后,range循环结束

相较于数组和切片,channel中的元素没有下标的概念,所以其最多只能返回一个元素。此外需要格外留意的是,range会阻塞等待channel中的数据,直到channel被关闭,同时,如果range作用于值为nil 的 channel时,则会永久阻塞。

make和new关键字的区别

make:主要用于创建并初始化内置的复合数据类型,即切片(slice)、映射(map)和通道(channel)。对于切片,会根据传入的长度和容量参数分配底层数组的内存,并初始化切片的长度和容量属性等。对于映射,会初始化内部的哈希表结构,以便高效地存储和查找键值对。对于通道,会根据是否有缓冲区以及缓冲区大小等情况进行相应的内存分配和初始化,保证通道能够正确地进行数据通信。
new:只是简单地分配足够的内存来存储指定类型的数据,然后将这块内存清零,使存储的是该类型的零值,并没有对复合数据类型进行像make那样的复杂初始化。例如,对于基本数据类型,只是返回指向零值的指针;对于结构体,只是将结构体的每个字段初始化为零值后返回指针。如果使用new创建切片(slice)、映射(map)和通道,则需要额外的 make 操作进行初始化才能使用。

slice(切片)、map(映射)和 chan(通道)这几种数据类型在使用前通常需要进行初始化
切片可以直接创建用append会自动初始化或用语法糖字面量初始化,map可以使用字面量初始化,通道必须用make初始化。

二、可见性

1,声明在函数内部,是函数的本地值,类似private
2,声明在函数外部,是对当前包可见(包内所有.go⽂件都可见)的全局值,类似protect
3,声明在函数外部且首字母⼤写是所有包可见的全局值,类似public
4,有四种主要声明方式 var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)

三、基本数据类型

整数类型

类型批注范围
int与平台相关,32位系统上通常是32位,64位系统上通常是64位32 位系统上范围是 - 2147483648(-231)到 2147483647(231-1)
int88位 适用于明确知道数据范围且对内存占用要求精确的情况。比如在网络协议解析中,如果协议规定一个整数字段是 8 位有符号整数,就可以使用int8来接收和处理这个数据- 128(-27)到 127(27
int1616位- 32768(-215)到 32767(215-1)
int3232位- 2147483648(-231)到 2147483647(231-1)
int6464位- 9223372036854775808(-263)到 9223372036854775807(263-1)
uint与平台相关,32位系统上通常是32位,64位系统上通常是64位32 位系统上范围是 0 到 4294967295(232-1)
uint8别名byte,8位,主要用于处理字节流,如文件读取、网络传输的数据字节流等。0 到 255(28-1)
uint1616位0 到 65535(216-1)
uint3232位0 到 4294967295(232-1)
uint6464位0 到 18446744073709551615(264-1)
rune本质是int32,即 32 位,用于处理 Unicode 字符。由于 Go 语言字符串是 UTF - 8 编码,一个字符可能占用多个字节,rune可以准确地表示一个 Unicode 码点,方便字符处理。- 2147483648(-231)到 2147483647(231-1)
uintptr一个无符号整数类型,可存储指针的地址。通常与 unsafe 包结合使用,用于低级编程和指针运算,但使用时需谨慎,避免引发内存安全问题。其位数未定义,通常和机器的字长有关,在 32 位机器上可能是 32 位,在 64 位机器上可能是 64 位。

浮点数类型

类型批注范围
float32使用时用math标准库 单精度浮点数,32 位(4 字节)按照 IEEE 754 标准存储,1 位表示符号(正或负),8 位表示指数,23 位表示尾数。精度:大约 6 - 7 位有效数字。范围:大约是 - 3.403×1038到 3.403×1038
float64使用时用math标准库 双精度浮点数,64 位(8 字节)1 位表示符号,11 位表示指数,52 位表示尾数。精度:大约 15 - 16 位有效数字,能提供更高的精度。范围:大约是 - 1.798×10308到 1.798×10308

复数类型

类型批注范围
complex64由两个float32组成,总共 64 位(8 字节),实部和虚部各占 32 位。用于复数运算,在信号处理、电气工程等领域有应用,如表示交流电路中的电压、电流的复数形式。大约是 - 3.403×1038到 3.403×1038。只要其实部和虚部满足 float32 的取值范围即可。
complex128由两个float64组成,总共 128 位(16 字节),实部和虚部精度更高。:用于对精度要求更高的复数运算场景,如高精度的信号处理算法等。大约是 - 1.798×10308到 1.798×10308。只要其实部和虚部满足 float64 的取值范围即可。

字符串型

类型批注范围
string存储:字符串是一个字节切片([]byte)加上一个长度标识。字节数取决于字符串内容的长度。由于是 UTF - 8 编码,字符占用字节数不定,ASCII 字符占 1 个字节,非 ASCII 字符可能占多个字节。用途:存储文本信息,在文本处理、用户输入输出、文件读取后的文本存储等场景广泛使用。字符串的长度(即字符串中字符的数量)理论上没有严格的固定上限。Go 语言的字符串可以包含任意 Unicode 字符,从简单的 ASCII 字符到复杂的表意文字、符号等,只要符合 UTF - 8 编码规则即可。

声明一个空字符串变量再赋值:

var sl string
sl = "Hello World"
//需要注意的是空字符只是长度为0,但不是nil。

字符申拼接时会触发内存分配及内存拷贝,单行语句拼接多个字符串只分配一次内存 比如下面的语句:

s = s + "a"+"b"
//在拼接时,会先计算最终字符串的长度后再分配内存,即便有非常多的字符串需要拼接,性能上也有比较好的保证,因为新字符串得内存空间是一次分配完成的,所以性能消耗主要在拷贝数据上。

字符串变量可以接受新的字符串赋值,但不能通过下标的方式修改字符串中的值。比如下面的代码,尝试将字母"H"修改为小写:

s := "hello"
&s[0] = byte(104) // 非法 
s = "hello" // 合法
//字符串不支持取地址操作,也就无法修改字符串的值

字符串不可以修改,字符串在底层是只读的字节切片。
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。⽆论哪种转换,都会发生一次内存拷贝,会有一定开销。
字符串使用Unicode编码存储字符,字符串长度是指Unicode编码所占的字节数。
string 可能为空,但不会是nil。
当你使用 + 运算符或 fmt.Sprintf 等函数来拼接字符串时,Go 会自动为你处理字符串的复制和内存分配,你不需要手动将字符串转换为 []rune 或 []byte。

正确实现字符串拼接的方式
可以使用 + 运算符来拼接字符串,不过要使用双引号 " 来表示字符串。

package main

import (
    "fmt"
)

func main() {
    str := "abc" + "123"
    fmt.Println(str)
}


fmt.Sprintf 函数可以根据格式化字符串生成一个新的字符串,也能用于字符串拼接。

package main

import (
    "fmt"
)

func main() {
    str := fmt.Sprintf("%s%s", "abc", "123")
    fmt.Println(str)
}


当需要拼接大量字符串时,使用 strings.Builder 效率更高,它避免了频繁创建新的字符串对象。

package main

import (
    "fmt"
    "strings"
)

func main() {
    var builder strings.Builder
    builder.WriteString("abc")
    builder.WriteString("123")
    str := builder.String()
    fmt.Println(str)
}



rune类型
Go 语⾔里的字符串的内部实现使用UTF-8编码,需要处理中⽂、日⽂或者其他复合字符时,则需要用到rune类型。rune类型实际是⼀个int32。Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode的⽂本处理更为⽅便。

/ /遍历字符串
func traversalString() {
s := "pprof.cn博客"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
}
输出:
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174() 162(¢)
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338() 23458()

因为UTF8编码下⼀个中⽂汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历⼀个包含中⽂的字符串,否则就会出现输出中第⼀⾏的结果。
对于包含多字节编码字符的字符串,使用 range 关键字遍历是一种更方便和正确的方式,因为它自动处理字符的编码问题。

%c 专门用于格式化输出 Unicode 字符
%v 是一个通用的占位符,它可以用于输出任意类型的值。当使用 %v 时,Go 语言会根据值的类型使用默认的格式来输出。对于基本数据类型(如整数、浮点数、字符串等),会输出其实际的值;对于结构体,会输出结构体的字段名和对应的值;对于切片、数组等,会输出其元素
%d 是用于格式化输出整数的占位符,它会将对应的整数参数以十进制的形式输出。
\n 是一个转义字符,表示换行符。
%s:用于输出字符串

如果你想使用 for 循环而不是 range 来正确遍历字符串中的字符,可以将字符串转换为 []rune 类型,这样就可以基于字符而不是字节来遍历:

func traversalString2() {
    s := "pprof.cn博客"
    runes := []rune(s)
    for i := 0; i < len(runes); i++ {
        fmt.Printf("%v(%c) ", runes[i], runes[i])
    }
    fmt.Println()
}

如果你想使用 for 循环和 len 函数,将字符串转换为 []rune 可以避免多字节字符处理时的问题,但需要注意的是,这种转换会涉及到额外的内存分配,因为创建了一个新的 []rune 切片。

布尔型

类型批注范围
bool通常占用 1 个字节(8 位),虽然理论上只需要 1 位来表示两个状态(true和false),但计算机存储单元一般以字节为最小分配单位。用于条件判断。两个状态(true和false)。

Go 语言中有哪些基本数据类型?请举例说明它们的声明和初始化方式。
Go 语言的基本数据类型有 布尔型(bool)、整型(int、int8、int16、int32、int64等)、浮点型(float32、float64)、复数型(complex64、complex128)、字符串(string)和字节型(byte)。例如,声明和初始化一个整型变量可以是var num int = 10或者num := 10(使用短变量声明);声明一个字符串可以是var str string = "Hello"或者str := “World”。

三、复合数据类型

管道

管道是一种用于在协程(Goroutine)之间进行通信和同步的机制。它可以被看作是一个先进先出的队列,协程可以通过管道发送和接收数据。管道的类型是chan,可以用于传递各种类型的数据,如整数、字符串、结构体等。
内置函数len()和cap()作用于管道,分别用查询缓冲区中数据的个数,以及缓冲区的大小。无缓冲的管道cap是0.
管道必须使用make初始化。管道的读写操作是原子性的,就是说一次只能有一个协程进行写操作或者读操作。
⽆缓冲的channel是同步的,⽽有缓冲的channel是⾮同步的。

创建管道的方法(初始化管道)

使用 make 函数初始化无缓冲通道

package main
import "fmt"
func main() {
    // 初始化一个无缓冲的 int 类型通道
    ch := make(chan int)
    go func() {
        ch <- 42
        close(ch)
    }()
    // 从通道接收数据
    value := <-ch
    fmt.Println(value)
}

使用 make 函数初始化有缓冲通道

package main
import "fmt"
func main() {
    // 初始化一个容量为 2 的有缓冲 int 类型通道
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    // 从通道接收数据
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

无缓冲管道(Unbuffered Channel)和有缓冲管道(Buffered Channel)的区别

无缓冲管道:无缓冲管道的发送和接收操作是同步的。 当一个协程向无缓冲管道发送数据时,必须有另一个协程同时准备好从管道接收数据,否则发送操作会阻塞。类似的,向管道写入数据也会阻塞,直到有协程从管道读取数据。
有缓冲管道:有缓冲管道在创建时指定了一个缓冲区大小。发送操作会将数据放入缓冲区,如果缓冲区未满,发送操作不会阻塞;接收操作会从缓冲区中取出数据,如果缓冲区中有数据,接收操作也不会阻塞。只有当缓冲区满时,发送操作才会阻塞;当缓冲区空时,接收操作才会阻塞有缓冲管道可以在一定程度上缓解协程之间的速度差异,提高程序的并发性能。例如,在一个生产者生产数据速度较快,而消费者消费数据速度较慢的场景中,有缓冲管道可以暂存生产者生产的数据,避免生产者因为消费者来不及接收而频繁阻塞。

声明管道
var ch chan int //变量声明

这种方式声明的管道,值为nil,无论读写都会阻塞,而且是永久阻塞。

管道读取表达式最多可以给两个变量赋值:
data, ok := <-ch

第一个变量表示读出的数据,第二个变量(bool类型)表示是否成功读取了数据。
判断通道是否关闭应该依据 ok 的值。当 ok 为 false 时,表示通道已经关闭且没有更多数据可供接收;当 ok 为 true 时,通道可能未关闭,也可能关闭但还有剩余数据。

管道的关闭

可以使用close函数来关闭管道。尝试向关闭的管道写入数据会触发panic,但关闭的管道仍可读。
管道关闭:不可写入。

关闭后的通道有以下特点:
1.对⼀个关闭的通道再发送值就会导致panic。
2.对⼀个关闭的通道进⾏接收会⼀直获取值直到通道为空。
3.对⼀个关闭的并且没有值的通道执⾏接收操作会得到对应类型的零值。
4.关闭⼀个已经关闭的通道会导致panic

通道与文件关闭的区别

通道关闭不是必须的
通道在 Go 语言的垃圾回收机制下是可以被自动回收的。如果通道没有被其他goroutine引用并且不再使用,Go 的垃圾回收器会回收它占用的内存资源。而且在很多情况下,不关闭通道也不会导致程序错误或内存泄漏等问题。例如,当通道用于信号通知或者在一个长期运行的程序中持续传递数据时,可能不需要关闭通道。
文件关闭是必须的
与通道不同,文件操作后必须关闭文件。这是因为文件占用操作系统的资源,如文件描述符等。如果不关闭文件,这些资源不会被释放,会导致资源泄漏。当一个文件被打开后,操作系统会为其分配一定的资源用于文件的读写操作。如果程序一直打开大量文件而不关闭,会耗尽系统的资源,最终可能导致程序或系统出现问题

管道的遍历

可以使用range关键字来遍历管道中的数据。当管道被关闭时,range循环会自动结束。这种方式使得从管道接收数据并处理的过程更加简洁。

package main

import "fmt"

func main() {
    // 创建一个int类型的管道
    ch := make(chan int)

    // 开启一个协程往管道写入数据
    go func() {
        for i := 0; i < 3; i++ {
            ch <- i
        }
        close(ch) //注意要关闭管道,不然range遍历可能会一直阻塞等待更多数据
    }()

    // 使用range遍历管道数据并打印
    for num := range ch {
        fmt.Println(num)
    }
}

管道阻塞

协程读取管道时,阻塞的条件有:管道无缓冲区,管道的缓冲区中无数据,管道的值为nil。
协程写入管道时,阻塞的条件有:管道无缓冲区,管道的缓冲区已满,管道的值为nil。
因读阻塞的协程会被向管道写入数据的协程唤醒。
因写阻塞的协程会被从管道读取数据的协程唤醒。

会触发panic的操作
关闭值为nil的管道
关闭已经被关闭的管道
向已经关闭的管道写入数据

通道有三种状态:正常状态、已关闭状态和 nil 状态。当通道为 nil 时,对其进行发送和接收操作都会使当前协程陷入阻塞状态,且这种阻塞不会被解除,除非程序崩溃或者有其他特殊操作(但通常不会出现这种情况)。这是因为 nil 通道没有分配内存空间,无法进行数据的发送和接收操作。

请解释协程和管道在 Go 语言并发编程中的作用以及它们是如何配合使用的?

协程是一种轻量级的线程,由 Go 语言的运行时系统管理。它们可以在单个线程上运行多个并发任务,比传统的线程更轻量,启动和销毁的开销更小。
可以使用 go 关键字来启动一个协程。

func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Printf("%d ", i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // 启动一个协程
    go printNumbers()
    // 主协程继续执行
    time.Sleep(1 * time.Second)
}
代码解释:
go printNumbers():使用 go 关键字启动一个协程,该协程会并发执行 printNumbers 函数。
time.Sleep(1 * time.Second):主协程睡眠 1 秒,给 printNumbers 协程足够的时间来执行。

管道(channel)的作用

==管道是一种用于在协程之间进行通信和同步的机制。==它们可以安全地传递数据,避免了共享内存带来的数据竞争问题。
可以创建无缓冲或有缓冲的管道,分别具有不同的读写特性。

协程和管道的配合使用

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func worker(c chan bool) {
    defer wg.Done()
    // 执行一些工作
    c <- true
}

func main() {
    c := make(chan bool)
    wg.Add(1)
    go worker(c)
    // 等待工作完成
    <-c
    wg.Wait()
}
var wg sync.WaitGroup:用于等待协程完成。
wg.Add(1):增加等待计数。
go worker(c):启动一个协程。
<-c:主协程等待 worker 协程发送的数据,实现同步。

管道可以在协程之间传递数据。

package main

import "fmt"

func producer(c chan int) {
    for i := 1; i <= 5; i++ {
        c <- i
    }
    close(c)
}

func consumer(c chan int) {
    for value := range c {
        fmt.Println(value)
    }
}

func main() {
    c := make(chan int)
    go producer(c)
    consumer(c)
}
producer 协程向管道发送数据。
consumer 协程从管道接收数据,使用 for...range 遍历管道,直到管道关闭且管道中没有数据。
虽然只有 producer 函数是以协程(goroutine)的形式启动,但consumer 函数在主协程(主 goroutine)中执行,这两个函数仍然是并发执行的

多个协程可以使用管道进行通信和协作。

package main

import (
    "fmt"
    "sync"
)

func worker(id int, c chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    value := <-c
    fmt.Printf("Worker %d received %d\n", id, value)
}

func main() {
    var wg sync.WaitGroup
    c := make(chan int)
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, c, &wg)
    }
    c <- 42
    wg.Wait()
}
多个 worker 协程等待从管道接收数据。
主协程发送数据,多个 worker 协程接收并处理数据。

如果不传递 wg 的指针(即使用 &wg),而是直接传递 wg,那么每个 worker 协程接收到的将是 wg 的一个副本。每个副本都有自己独立的计数器,这会导致 main 函数中的 wg 计数器不会被 worker 协程中的 Done 方法正确更新。
通过传递 wg 的指针 &wg,所有的 worker 协程都能访问到同一个 WaitGroup 实例,这样 worker 协程调用 Done 方法时,会正确地减少 main 函数中 wg 的计数器,从而使 wg.Wait() 能够正常工作,等待所有 worker 协程完成任务。

数组

数组是具有固定长度的、存储相同类型元素的序列。在声明数组时,必须明确指定数组的长度,且该长度在数组的整个生命周期内都不能改变
例如,var a [5]int定义了一个长度为 5,元素类型为int的数组。
也可以在定义时初始化,如b := [3]string{“apple”, “banana”, “cherry”},这就定义并初始化了一个包含 3 个字符串元素的数组。
数组长度是类型的一部分,所以[3]int和[4]int是不同类型。元素在内存中是连续存储的,通过索引访问元素速度快,索引从 0 开始。
应用场景
适用于存储固定数量且类型相同的数据,如存储一周七天的名称,或者一个班级学生的成绩列表

当数组是在函数内部定义的局部变量,并且其大小在编译时可以确定,同时没有发生逃逸(即数组不会在函数外部被引用),那么这个数组会被分配在栈上。栈内存的分配和释放速度非常快,由编译器自动管理。
如果数组发生了逃逸,即数组会在函数外部被引用,或者数组的大小在编译时无法确定,那么这个数组会被分配在堆上。堆内存的管理相对复杂,需要垃圾回收机制来回收不再使用的内存。

删除元素
数组长度是固定的,不能直接删除元素。不过可以通过创建一个新数组,将不需要删除的元素复制到新数组中来间接实现 “删除”。

package main

import (
    "fmt"
)

func main() {
    // 定义一个数组
    arr := [5]int{1, 2, 3, 4, 5}
    var newArr [4]int
    indexToRemove := 2
    j := 0
    for i := 0; i < len(arr); i++ {
        if i != indexToRemove {
            newArr[j] = arr[i]
            j++
        }
    }
    fmt.Println(newArr)
}

切片slice


总结

未完待续

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值