FaceToFace G
1.21(1.22 1.24 1.28)
func main() {
list := new([]int)
//这里意味着list是一个 *[]int 类型的指针
list = append(list, 1)
//不能对指针进行append操作
fmt.Println(list)
}
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s1 = append(s1, s2)
//error
//将一个切片追加到另一个切片上:append(s1,s2...)
fmt.Println(s1)
}
var(
size := 1024
max_size = size*2
)
//error
//只能在函数内部使用简短模式
func main() {
fmt.Println(size,max_size)
}
func main() {
slice := []int{0,1,2,3}
m := make(map[int]*int)
for key,val := range slice {
m[key] = &val
//val的每次地址都是一样的,只是值不一样
/*正确写法
value := val//用一个新的地址保存新的值
m[key] = &value
*/
}
for k,v := range m {
fmt.Println(k,"->",*v)
}
}
// 1.
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
0 0 0 0 0 1 2 3
// 2.
func main() {
s := make([]int,0)
s = append(s,1,2,3,4)
fmt.Println(s)
}
1 2 3 4
new(T) 和 make(T,args) 是 Go 语言内建函数,用来分配内存,但适用的类型不同。
new(T) 会为 T 类型的新值分配已置零的内存空间,并返回地址(指针),即类型为 *T
的值。换句话说就是,返回一个指针,该指针指向新分配的、类型为 T 的零值。适用于值类型,如数组、结构体等。
make(T,args) 返回初始化之后的 T 类型的值,这个值并不是 T 类型的零值,也不是指针 *T
,是经过初始化之后的 T 的引用。make() 只适用于 slice、map 和 channel.
func main() {
sn1 := struct {
age int
name string
}{age: 11, name: "qq"}
sn2 := struct {
age int
name string
}{age: 11, name: "qq"}
if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}//success
sm1 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
sm2 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
if sm1 == sm2 {
fmt.Println("sm1 == sm2")
}//error
}
结构体比较有几个需要注意的地方:
结构体只能比较是否相等,但是不能比较大小
相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与顺序有关
一般来说 普通类型如数组,指针
,字符,布尔值可以进行比较
切片,map,channel,函数不可以
1.22(1.23 1.25 1.29)
1.23 已复习
type:
type MyInt int //实现了int的新类型
type MyInt2 = int;//int的别名->MyInt
var i int = 0
var myInt MyInt = i;
/*error
Go 是强类型语言,int类型不能赋给MyInt
correct:
var myInt Myint = (Myint)i
*/
字符串连接
str := "abc"+"123"
str := fmt.Sprinf("%s is %s year old","Jack","eight")
//strings.Join
str1 = []string{"I love you"}
str :=strings.Join(str1," ");
//首先,我们定义了一个名为str1的字符串切片,内容是"I love you"
//然后我们使用了strings.Join,实现了str1和" "的拼接
//buffer.WriteString
str1 := "Hello"
str2 :="HaiCoder"
var lk bytes.Buffer
lk.WriteString(str1)
lk.WriteString(str2)
strLk := lk.String()
/*首先,我们定义了两个字符串变量并赋值,然后我们定义了bytes.Buffer类型的变量
然后,我们使用了writeString的方法将str1和str2均写入进去
最后,我们使用String方法将这两者拼接,并把结果赋给strLk
*/
iota
const (
x = iota
_
y
z = "zz"
k
p = iota
)
func main() {
fmt.Println(x,y,z,k,p)
}
/*
iota是golang语言的常量计数器,只能在常量的表达式中使用
iota在其出现的时候,变量x被重置为0,之后的每一行变量声明都会使iota计数一次
当遇到下划线的时候,同样会技术,通过下划线可以去掉不想要的值
//当遇到z= "zz"这样类型的值的时候,iota同样会计数
//但此时如若后面没有再出现变量p=iota 类似的子句
后面的变量会赋值为zz
同样,如果出现u = "hh"后面的也都会变成hh
*/
nil
nil 只能赋给指针,chan,func,interface,map或slice类型的变量
error是一种内置接口类型
type error interface{
Error () string
}
init()
在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。
需要注意的是:init()
函数没有参数,也没有返回值。
init()
函数在程序运行时,自动自动被调用执行,不能在代码中主动调用它。
包初始化执行的顺序如下图所示
关于 init() 函数有⼏个需要注意的地⽅:
-
- init() 函数是⽤于程序执⾏前做包的初始化的函数,⽐如初始化包⾥的
- ⼀个包可以==出现多个 init() 函数,==⼀个源⽂件也可以包含多个 init() 函数;
- 同⼀个包中多个 init() 函数的执⾏顺序没有明确定义,但是不同包的init函数是根据包导⼊的依赖关 系决定的;
- init() 函数在代码中不能被显示调⽤、不能被引⽤(赋值给函数变量),否则出现编译错误;
- ⼀个包被引⽤多次,如 A import B,C import B,A import C,B 被引⽤多次,但 B 包只会初始化⼀ 次;
- 引⼊包,不可出现死循坏。即A import B,B import A,这种情况编译失败;
接口
i.(type),其中 i 是接⼝,type 是固定 关键字,需要注意的是,只有接⼝类型才可以使⽤类型选择
1.23(1.24 1.26 1.30)
channel
Channel 是 Go的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯
//它的操作符是箭头<-
ch <- v //发送值v到Channel ch中
v := <-ch //从Channel ch 中接收是数据,并将值赋给v
//和map、slice一样,channel必须先创建后使用
ch := make(chan int)
三种类型
chan T //可以接收和发送类型为T的数据
chan<- float64 //只可以用来发送float64类型的数据
<-chan int //只可以用来接收 int类型的数据
使用make初始化Channel,并且可以设置容量
make(chan int,100)
capcity: Channel容纳最多元素的数量,代表Channel的缓存的大小
如果没有设置容量,只有sender和receiver都准备好了,通讯才会发生
如果设置了容量,只有buffer(缓存)满了,sender才会阻塞
一个nil channel 不会通信
这里要插入一个goroutine (并发)
让我们来理解一下
goroutine是Go语言中最大的特色,Goroutinue是Go中最基本的执行单元
线程(Thread):轻量级进程,是进程中的一个实体,不拥有系统资源,只有在运行中必不可少的资源,与同属于一个进程的其他线程共享进程所拥有的全部资源
协程(coroutinue) :
与函数一样,是一种程序组件
和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显示控制,避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂
goroutine和协程类似,goroutine可以理解为一种Go语言的协程
go语言有和java类似的多线程共享内存
还有一个特点:通过通信来共享内存(CSP)
Go的CSP并发模型,是通过goroutine
和channel
来实现的。
goroutine
是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。channel
是Go语言中各个并发结构体(goroutine
)之前的通信机制。 通俗的讲,就是各个goroutine
之间通信的”管道“,有点类似于Linux中的管道。
数据用channel <- data
,取数据用<-channel
在通信过程中,传数据channel <- data
和取数据<-channel
必然会成对出现,因为这边传,那边取,两个goroutine
之间才会实现通信。
而且不管传还是取,必阻塞,直到另外的goroutine
传或者取为止。
package main
import "fmt"
func main() {
messages := make(chan string)
go func() { messages <- "ping" }()
msg := <-messages
fmt.Println(msg)
}
/* main()本身也是运行了一个goroutine。
messages:= make(chan int) 这样就声明了一个阻塞式的无缓冲的通道
chan 是关键字 代表我要创建一个通道
回到channel上来
可以通过内建的close
方法可以关闭Channel。
你可以在多个goroutine从/往 一个channel 中 receive/send 数据, 不必考虑额外的同步措施。
Channel可以作为一个先入先出(FIFO)的队列,接收的数据和发送的数据的顺序是一致的。
v,ok := <-ch
func hello(num ...int) {
num[0] = 18
}
func main() {
i := []int{5, 6, 7}
hello(i...)
fmt.Println(i[0])
}
t := a[3:4:4]
截取操作符还可以有第三个参数,形如 [i,j,k],第三个参数 k ⽤来限制新切⽚的容量,但不能超过原数组 (切⽚)的底层数组⼤⼩。截取获得的切⽚的⻓度和容量分别是:j-i、k-i。
所以例⼦中,切⽚ t 为 [4],⻓度和容量都是 1
a := [2]int{5,6}
b:=[3]int{5,6}
a和b可以进行比较?
Go中的数组是值类型,可比较,
但是数组的长度也是数组类型的组成部分,所以a和b是不同的类型
cap
package main
func main() {
array := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := array[0:5]
temp := cap(slice)//slice最左边的指针索引是0,到结尾有9个数
println(temp)
slice = slice[2:]
println(cap(slice))//此时最左边的索引为2,所以为7
}
接口
在Golang中一个接口由两个部分组成,分别是动态类型和动态值。
在一个时刻,一个接口只能有一个类型和值,这是这个接口该时刻的具体类型和具体值。
因为一个接口可以有多个结构体实现,所以当不同的实现该接口的结构体赋值给一个接口变量的时候,接口的动态类型会变成该结构体类型,接口内部存储的指向结构体的指针会指向当前赋值的结构体,而动态值会变成当前赋值的结构体的值
有且仅当接口的动态值和动态类型都为nil时,接口类型为nil
func mian(){
s:=make(map[string] int)
delete(s,"h")//删除map不存在的键值对时,不会报错,相当于没有任何作用
fmt.Println(s["h"])
//获取不存在的键值对时,返回值类型对应为零值
}
Go语言有25个预定义的关键字,这些关键字用于各种特定目的,从声明变量和函数,到流程控制和并发编程。
break, default, func, interface, select, case, defer, go, map, struct, chan, else, goto, package, switch, const, fallthrough, if, range, type, continue, for, import, return, var
i:=-5
fmt.Printf("%+d",i)//+标识输出数值的符号,不表示取反
//输出-5
全局变量
在Go中,全局变量指的是在包的最顶层声明的头母大写的导出变量,这样这个变量在整个Go程序的任何角落都可以被访问和修改,比如下面示例代码中foo包的变量Global:
package foo
var Global = "myvalue" // Go全局变量
var Str string//只有这两种形式
//不能出现 str:= " "
package bar
import "foo"
func F1() {
println(foo.Global)
foo.Global = "another value"
}
defer
func hello(int i){
fmt.Println(i)
}
func main(){
i:= 5
defer.hello(i)
i += 10
}
//输出为5,因为执行到defer.hello时,我们已经保存了副本i
//defer虽然是在函数结束之后执行,但与副本无关
有关结构体嵌套
type People struct{}
func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
}
在这个代码中,People称为内部类型,Teacher为外部类型,外部类型可以将内部类型的属性和方法当作自己的使用,但是如果外部类型出现与内部类型同样的属性和方法的时候,会屏蔽掉内部类型的方法与属性
字符串
package main
import "fmt"
func main() {
str := "hello"
str[0] = 'x'
fmt.Println(str)
}
常量,Go 语⾔中的字符串是只读的。
nil切片和空切片
func main(){
var s1 []int //nil切片
var s2 = []int{}//空切片
}
可以进行强转 int->string
切片a,b,c的长度和容量分别是多少?
func main(){
s:= [3]int{1,2,3}
a := s[:0]
b := s[:2]
c := s[1:2:cap(s)]
}
假设截取对象的底层数组长度为l
形如[i:j] [i:j:k] 截取得到的切片长度和容量分别是 j-i,l-i
k主要用来限制容量
切片长度和容量分别是 :j - i,k-i
1.24(1.25 1.27 1.31)
1.25 已过
1.31 已过
//规定x已声明,y没有声明
x,_ := f() // x已经声明,:=只对没有声明的进行定义并赋值
x,_ = f() //对
x,y :=f() //对,当多值赋值的时候,:=左边的变量无论声明与否都可以
x,y = f() //错,y没有声明
defer 与返回值的关系
当 defer
语句与函数返回值一同使用时,需要注意以下几点:
-
延迟执行的是函数调用,而不是函数内的具体语句。
-
defer
语句中的函数调用会使用函数定义时的参数值。 -
defer
语句的执行时机是在包含它的函数执行完毕后,但在返回之前。package main import "fmt" func example() (result int) { defer func() { // 在函数返回前执行,可以修改返回值 result++ fmt.Println("Deferred function executed") }() fmt.Println("Executing main function") return 42 } func main() { value := example() fmt.Println("Returned value:", value) }
在这个例子中,
defer
语句中的匿名函数会在example
函数返回前执行,可以在其中修改返回值。输出结果如下:
Executing main function
Deferred function executed
Returned value: 43
但是如果我们在函数中定义了一个具体的值就不一样了,他并不会改变具名
func increaseA() int {
var i int
defer func() {
i++
}()
return i
}
我们可以发现在这段代码中,i是一个具名,这也就意味这defer中的匿名函数保存的是i的副本,并不会改变i地址的值
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)//这个传的是r的拷贝
return 1
闭包
闭包是一个函数值,它引用了函数体之外的变量,这些变量可以是外部函数的参数,也可以是外部函数内部声明的变量
第一个保存的是age的值,存入栈中,当这条语句被执行的时候,直接将存入的值取出 28
2.第二个保存的是p的地址,所以最后person中地址的值被修改,这里也同样会被修改
3.闭包引用的是地址,所以是一样的
最后一行修改的是对象中age的地址,person并没有改变哈
如果改变的话
person = &Person{29}
声明
通常选用不分配内存的
var a []int
永远不要使用一个指针指向一个接口类型,因为它已经是一个指针了
func g(x *interface{}) //这个是错误的
接收指针类型也可以用interface()
函数参数为interface{}时可以接收任何类型的参数,包括自定义类型
1.25(1.26 1.28 2.1)
var a string = nil
//error golang 的字符串类型不能赋值nil,也不能和nl比较
//string 的零值是“”
defer只有在return前面才会执行
切片是从0开始的
s2 :=s1[1:]//从s1第二个元素开始切,直到后面没有元素
s2 = append(s2,5,6,7)//append会导致底层数组扩容,生成新的数组,此时的s2成为一个新数组
func main() {
if a := 1; false {
} else if b := 2; false {
} else {
println(a, b)
}
}
这段代码可以转成
func main() {
{
a := 1
if false {
} else {
{
b := 2
if false {
} else {
println(a,b)
}
}
}
map的输出是无序的!!!!
func main(){
m:map[int]string{0:"zero",1,"one"}
for k,v:range m{
rmt.Println(k,v)
}
}
10 1 2 3 //3
calc(“1”,a,10)
1 1 2 11 //4
20 0 2 2 //1
clac(“2”,1,4)
2 1 4 5 // 2
defer 里面的函数会优先执行(按照正常顺序)
defer calc("1",a,calc("10",a,b))//会优先执行cal("10",a,b)
基于类型创建的方法必须定义在同一个包里
func ( i int) PrintInt(){
fmt.Println()
}//这个方法必须要定义在int包里面
//普通包是运行不了的
func(stu * Student) Speak(think string)(talk string){
}
//这个是指针类型*Student实现了该方法 不是值类型Student
当一个类型实现了某个接口的方法时,只有该类型的指针类型才会被自动视为实现了该接口
只有实现了接口方法的指针类型才会被自动视为实现了该接口。如果要将值类型赋值给接口,必须确保值类型本身也实现了接口的方法。
假设有字符 ‘a’ 和字符 ‘b’,它们的Unicode码点分别为 97
和 98
。如果你执行 'b' - 'a'
,实际上就是在进行 98 - 97
的整数运算,结果是 1
。
1.26(1.27 1.29 2.2)
iota是golang语言的常量计数器,只能在常量的表达式中使用。
iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
package main
import "fmt"
type People interface {
Show()
}
type Student struct{}
func (stu *Student) Show() {
fmt.Println("Showing student information")
}
func main() {
var s *Student
if s == nil {
fmt.Println("s is nil")
} else {
fmt.Println("s is not nil")
}
var p People = s
if p == nil {
fmt.Println("p is nil")
} else {
fmt.Println("p is not nil")
p.Show()
}
}
s is nil
结构体指针:指向Student类型的指针,但它没有被初始化,如果一个指针没有被初始化,就是nil
但如果一个结构体没有被初始化,它的所有字段都会变成零值,但是无法与nil进行比较
nil是引用类型的零值,但是结构体中的字段可以有引用类型,但还可以有字符串类型,int,这些都是具体值的类型,自然无法与nil进行比较
在 Go 语言中,即使接口内部的具体类型是 nil
,接口本身也不会等于 nil
接口变量未被初始化或赋值为 nil
: 如果你声明一个接口变量但没有分配具体的实例给它,它的零值将是 nil
。例如:
goCopy codevar myInterface MyInterface
// 此时 myInterface 是 nil
当一个接口变量被分配了一个具体的实例,无论该实例是否为 nil
,接口变量本身都不会是 nil
。在 Go 语言中,接口变量总是非 nil
的,只有当它未初始化或显式赋值为 nil
时才可能为 nil
。p is not nil
切片
type Math struct {
x, y int
}
var m = map[string]Math{
"foo": Math{2, 3},
}
func main() {
m["foo"].x = 4 //error :
//这段代码是错误的,因为map中的值是不可寻址的,而你要访问结构体字段的话就需要知道它的地址,这是不允许的,除非它的值本身是一个结构体指针
fmt.Println(m["foo"].x)
}
/* go 中的 map 的 value 本身是不可寻址的,因为 map 的扩容的时候,可能要做 key/val pair迁移
value 本身地址是会改变的*/
//那我们只要知道被修改值的地址,就可以修改了
var m = map[string]Math{
"foo":&Math{2,3}
}
切片是不能比较的
fmt.Println([]int{1} == []int{1})
对于使⽤ := 定义的变量,如果新变 量与同名已定义的变量不在同⼀个作⽤域中,那么 Go 会新定义这个变量
如果在函数内部声明同名的局部变量,局部变量会覆盖全局变量。
- chatgpt
当一个指针被赋值为 nil
时,它表示该指针不指向任何有效的内存地址。因此,你可以说它没有有效的地址。尝试在 nil
指针上进行解引用或取地址的操作通常会导致运行时错误。
数组与切片
var a = [5]int{1,2,3,4,5}//注意这是一个数组
var r[5]int
for i,v:=range a{
if i == 0{
a[1] = 12
a[2] =13
}
r[i] = v
}
fmt.Printf("r = ",r)
//因为我们保存的是a的副本,而更改的是a本身的值
// 输出:[1,2,3,4,5]
var a = []int{1,2,3,4,5}//注意这是一个切片
var r[5]int
for i,v:=range a{//副本的指针依然指向底层数组
if i == 0{ //所以这层是有效的
a[1] = 12
a[2] =13
}
r[i] = v
}
fmt.Printf("r = ",r)
//所以这段代码的输出是[1,12,13,4,5]
go 提供了一种直接操作指针的方式,就是unsafe.Pointer 和 uintptr
uintptr 是一个整型,可理解为是将内存地址转换成了一个整数,既然是一个整数,就可以对其做数值计算,实现指针地址的加减,也就是地址偏移,类似跟 C 语言中一样的效果。
func main() {
a := 10
var b *int
b = &a
fmt.Printf("a is %T, a=%v\n", a, a)
fmt.Printf("b is %T, b=%v\n", b, b)
p := unsafe.Pointer(b)
fmt.Printf("p is %T, p=%v\n", p, p)
uptr := uintptr(p)
fmt.Printf("uptr is %T, uptr=%v\n", uptr, uptr)
}
// 输出结果
a is int, a=10
b is *int, b=0x140000ae008
p is unsafe.Pointer, p=0x140000ae008
uptr is uintptr, uptr=1374390247432
[]byte 的底层是一个指向连续内存块的指针和一个长度字段
string 的底层是一个指向 UTF-8 编码的字符串的指针和一个长度字段。
// 熟悉一下基本方法
/*
Contains() 返回是否包含子切片
Count() 子切片非重叠实例的数量
Map() 函数,将byte 转化为Unicode,然后进行替换
Repeat() 将切片复制count此,返回这个新的切片
Replace() 将切片中的一部分 替换为另外的一部分
Runes() 将 S 转化为对应的 UTF-8 编码的字节序列,并且返回对应的Unicode 切片
Join() 函数,将子字节切片连接到一起。
*/
(unsafe.Pointer)(&s)
:这里将&s
转换为unsafe.Pointer
类型。这是因为unsafe.Pointer
是一个通用指针类型,可以存储任何类型的指针。在Go语言中,将一个具体类型的指针转换为通用指针类型时,需要使用括号来明确转换的范围。(*byte)(p)
和(*int64)(p)
:这里将通用指针p
转换为*byte
和*int64
类型的指针。同样,括号用于明确将p
视为一个指针类型,而不是其他表达式的一部分。
总的来说,括号在这里的作用是为了确保类型转换的正确性,明确指定要转换的内容,避免歧义。这是Go语言中强制要求的语法规范
import (
"fmt"
"unsafe"
)
func main() {
s := struct {
a byte
b byte
c byte
d int64
}{a: 0, b: 0, c: 0, d: 0}
//将结构体指针转换为通用指针
p := unsafe.Pointer(&s)
//保存结构体的地址备用
up0 := uintptr(p)
//将通用指针转换为byte类型指针
pb := (*byte)(p)
//给转换后的指针赋值
*pb = 10
//结构体内容跟着变
fmt.Println(s)
//仅仅改变了第一个值(一个指针只能指向一个地址)
//我们让他偏移到第二个字段
/*func Offsetof(x任意类型) uintptr
Offsetof 返回 x 表示的字段在结构体中的偏移量,
该字段必须采用 structValue.field 形式。换句话说,它返回结构体开头和字段开头之间的字节数。*/
up := up0 + unsafe.Offsetof(s.b)
//将片以后的指针转为通用指针
p = unsafe.Pointer(up)
pb = (*byte)(p)
*pb = 20
fmt.Println(s)
up = up0 + unsafe.Offsetof(s.c)
//注意我们的up是个地址值
p = unsafe.Pointer(up)
pb = (*byte)(p)
*pb = 30
fmt.Println(s)
up = up0 + unsafe.Offsetof(s.d)
p = unsafe.Pointer(up)
pi := (*int64)(p)
*pi = 40
fmt.Println(s)
}
一旦数组被声明了,那么它的数据类型跟长度都不能再被改变。
var t *T
t = new(T)
上面简单的管用语句方法t := new(T)
,变量 t
是一个指向 T
的指针,此时结构体字段的值是它们所属类型的零值。
声明 var t T
也会给 t
分配内存,并零值化内存,但是这个时候 t
是类型T
。在这两种方式中,t
通常被称做类型 T
的一个实例(instance)或对象(object)。
- len(Array) 数组的元素个数
- len(*Array) 数组指针中的元素个数,如果入参为nil则返回0
- len(Slice) 数组切片中元素个数,如果入参为nil则返回0
- len(map) 字典中元素个数,如果入参为nil则返回0
- len(Channel) Channel buffer队列中元素个数
1.27(1.28 1.30 2.3)
func change(s ...int) {
s = append(s, 3)
}
func main() {
slice := make([]int, 5, 5)
slice[0] = 1
slice[1] = 2
change(slice...)
fmt.Println(slice)
change(slice[0:2]...)
fmt.Println(slice)
}
对于第一次传入 slice...
,实际上是将切片 slice
中的每个元素逐个传递给 change
函数。在 change
函数内部,这些元素被合并成一个新的切片 s
。然后,append(s, 3)
操作在这个新的切片上进行,但由于切片是值传递,这个 append
操作并不影响原始的 slice
。
在第二次传入 slice[0:2]...
的情况下,change
函数内部没有创建新的切片。相反,change
函数直接修改了传入的切片的底层数组。因为传递的是切片的引用,所以对切片底层数组的修改会影响到原始的切片。
type Foo struct {
bar string
}
func main() {
s1 := []Foo{
{"A"},
{"B"},
{"C"},
}
s2 := make([]*Foo, len(s1))
for i, value := range s1 {
s2[i] = &value
}
fmt.Println(s1[0], s1[1], s1[2])
fmt.Println(s2[0], s2[1], s2[2])
}
//关键是第二个
//我们说过 在for 中使用 := i和value都是重复的,所以他们地址是一样的,最后value被赋值为{"C"} 所以地址指向{C}
//实际的值就是 &{C}
func main() {
var m = map[string]int{
"A": 21,
"B": 22,
"C": 23,
}
counter := 0
for k, v := range m {
if counter == 0 {
delete(m, "A")
}
counter++
fmt.Println(k, v)
}
在你的代码中,你在循环的第一次迭代时删除了 map 中的键 “A”。这个删除操作导致了迭代器的失效,可能会导致后续迭代的不确定行为。具体来说,range
迭代器可能会因为元素的删除而发生改变,导致遍历过程中跳过某个元素或者遍历到不存在的元素。
这就解释了为什么 counter
的值有时候为 2,有时候为 3。在一些情况下,可能由于删除操作导致迭代器失效,使得第二次迭代时直接跳过了键 “B”,从而 counter
的值为 2。在另一些情况下,可能迭代器的变化不会导致直接跳过某个键,counter
的值为 3。
协程通过channel来进行协程间的通信
go只有for关键字支持循环
Go 的 for
语句提供了一个标签(label)的功能,可以选择中断指定的循环,这是一种更高级的 break
。
Go 中的 for
循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。
多重赋值
多重赋值分为两个步骤,有先后顺序:
计算等号左边的索引表达式和取址表达式,
接着计算等号右边的表达式; 赋值;
B.
type MyInt int
var i int = 1
var j MyInt = (MyInt)i
C.
type MyInt int
var i int = 1
var j MyInt = MyInt(i)
b合法但不道德(没有明确表明自己要表达的意思)
c合法道德,c明确表示了自己要表达的意思
switch
在go中,'break’语句通常是可选的
当匹配到一个case后,程序会自动退出switch,不需要显式添加’break’
如果要自动跳过某种情况,在case语句前使用’fallthrough‘可强制执行下一个case,这个跳过
方法
方法被 Integer
类型实现,但通过指针 *Integer
类型的实例也可以调用这个方法。Go 语言会在需要时自动进行值和指针之间的转换。
在Go中,布尔类型不支持直接的类型转换,而是通过使用布尔运算或比较运算符来实现。
语句是一段执行任务的代码,而表达式是一个计算值的代码片段。在Go中,大部分语句都包含一个或多个表达式。
自增自减
⾃增⾃减操作。i++ 和 i-- 在 Go 语⾔中是语句,不是表达式,因此不能 赋值给另外的变量。此外没有 ++i 和 --i
1.28(1.29 1.31 2.4)
函数声明
//这个是对的
func f(a,b int)(value int,err error)
//错误类型
func f(a,b int)(value int,error)//这里的error也要有变量名
切片
make([]T, length, capacity)
其中:
T
是切片的元素类型。length
是切片的长度,表示切片中元素的数量。capacity
是切片的容量,表示底层数组的大小,即切片可以扩展的最大长度。
map
在函数间传递 map 不是传递 map 的拷贝。所以如果我们在函数中改变了 map,那么所有引用 map 的地方都会改变
“别名类型,可以是中文!”
type (
byte int8
rune init32
文本 string
)
var b 文本
b = "别名类型,可以是中文!"
package main
import "unsafe"
// 常量可以用len(), cap(), unsafe.Sizeof()常量计算表达式的值。
// 常量表达式中,函数必须是内置函数,否则编译不过:
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)
如果你希望全局变量在包外部可见,其首字母应该大写;如果你希望全局变量在包外部不可见,其首字母应该小写。
//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"
在 if 的便捷语句定义的变量同样可以在任何对应的 else 块中使用。
switch 的执行顺序: 条件从上到下的执行,当匹配成功的时候停止。自动配有break
defer 语句会延迟函数的执行直到上层函数返回。 延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。
延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。
如果没有重新命名,那么对于编译器来说,这两个fmt
它是区分不清楚的。重命名也很简单,在我们导入的时候,在包名的左侧,起一个新的包名就可以了。
package main
import (
"fmt"
myfmt "mylib/fmt" //命名导入
)
func main() {
fmt.Println()
myfmt.Println()
}
但是有时候,我们需要导入一个包,但是又不使用它,按照规则,这是不行的,为此Go语言给我们提供了一个空白标志符_
,只需要我们使用_
重命名我们导入的包就可以了
package main
import (
_ "mylib/fmt"
)
包的init函数
每个包都可以有任意多个init函数,这些init函数都会在main函数之前执行。init函数通常用来做初始化变量、设置包或者其他需要在程序执行前的引导工作。
1.30(1.31 2.2 2.6)
1.31 已过
go的并发是指让某个函数独立于其他函数运行的能力,一个goroutine就是一个独立的工作单元,Go的runtime(运行时)会在逻辑处理器上调度这些goroutine来运行
概念 | 说明 |
---|---|
进程 | 一个程序对应一个独立程序空间 |
线程 | 一个执行空间,一个进程可以有多个线程 |
逻辑处理器 | 执行创建的goroutine,绑定一个线程 |
调度器 | Go运行时中的,分配goroutine给不同的逻辑处理器 |
全局运行队列 | 所有刚创建的goroutine都会放到这里 |
本地运行队列 | 逻辑处理器的goroutine队列 |
并行是做很多事情
并发是同时管理很多事情
而因为 操作系统和硬件的总资源比较少,所以并发的效果要比并行好的多
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2) // 计数的信号量,main函数等待两个 goroutine执行完成再结束
go func() {
defer wg.Done()
for i := 1; i < 100; i++ {
fmt.Println("A:", i)
}
}()
go func() {
defer wg.Done()
for i := 1; i < 100; i++ {
fmt.Println("B:", i)
}
}()
wg.Wait()
}
当创建一个goroutine后,会先存放在全局运行队列中,等待Go运行的调度器进行调度,把他们分配给其中的一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中,最终等着被逻辑处理器执行
我们对于同一个资源的读写必须是原子化的,也就是说同一时间只能有一个 goroutine对共享资源进行读写操作
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
value := atomic.LoadInt32(&count)
//atomic 包里面有很多原子性的操作
//这个是读取int32类型的值
runtime.Gosched() //让当前的gour
value++
atomic.StoreInt32(&count,value)
//这个是修改int32类型的值
}
}
sync包中提供了互斥锁,他能提供一种互斥型的锁,可以让我们灵活的控制那些代码,同时只能有一个goroutinue访问,被sync互斥锁控制的这u但代码范围,被称为临界区
临界区的代码,同一时间,只能由一个goroutine访问
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
mutex.Lock()
//
value := count
runtime.Gosched()
value++
count = value
//
//临界区
mutex.Unlock()
}
}
Go通道
我们可以使用通道在多个goroutinue发送和接受共享的数据,达到数据同步的目的
ch := make(chan int)
ch :=make(chan int,0)//无缓冲通道
无缓冲通道的代销为0
它要求发送goroutine和接收goroutine同时准备好,才可以完成发送和接收操作。
管道
package main
import "fmt"
func main() {
one := make(chan int)
two := make(chan int)
go func() {
one <- 100
}()
go func() {
v := <-one
two <- v
}()
fmt.Println(<-two)
}
var send chan<- int//只能发送
var receive <-chan int //只能接收
2.1(2.2 2.5 2.8)
控制某个程序是否终止可以使用complete无缓冲通道(同步)
package Runner
import (
"errors"
"os"
"os/signal"
"time"
)
type Runner struct {
tasks []func(int) //要执行的任务
complete chan error //用于通知任务全部完成
timeout <-chan time.Time //这些任务在多久内完成
interrupt chan os.Signal // 可以控制强制终止的信号
}
/*
我们定义一个结构体类型Runner
其中complete用于通知任务全部完成
不过有时间限制,就是timeout,他只能接收
complete定义为错误的类型,是因为当任务没有完成的时候
我们知道为什么错,如果没有错误,返回nil
*/
// 定义一个工厂函数 ,tm是超时时间
// tm会被传递给time.After函数,在tm后,会同伙一个time.Time类型的只能接收的单项通道来告诉我们已经到时间了
// complete是一个同步通道,因为我们要使用它来控制我们的整个程序是否终止
//interrupt 是一个有缓冲的通道,这样我们至少可以接收到一个操作系统的终端信息
//如果是无缓冲的通道就会阻塞
func New(tm time.Duration) *Runner {
return &Runner{
complete: make(chan error),
timeout: time.After(tm),
interrupt: make(chan os.Signal, 1),
}
}
// 通过方法给执行者添加需要执行的任务
func (r *Runner) Add(tasks ...func(int)) {
r.tasks = append(r.tasks, tasks...)
}
// 定义两个错误变量
var ErrTimeOut = errors.New("执行者执行超时")
var ErrInterrupt = errors.New("执行者被中断")
//执行任务,执行的过程中接收道中断信息时,返回中断错误
//如果任务全部执行完,还没有接收到中断信号
func (r *Runner) run() error {
for id, task := range r.tasks {
if r.isInterrupt() {
return ErrInterrupt
}
task(id)
}
return nil
}
//检查是否收到中断信号
/*
select和switch很像,
只不过它的每个case都是一个通信操作
如果没有default,select会阻塞
*/
func (r *Runner) isInterrupt() bool {
select {
case <-r.interrupt:
signal.Stop(r.interrupt)
return true
default:
return false
}
}
//开始执行我们的任务,并且监视通道事件
func (r *Runner) Start() error {
//希望接收哪些系统信号
//如果由系统中断的信号,发给r.Interrupt
signal.Notify(r.interrupt, os.Interrupt)
go func() {
r.complete <- r.run()
}()
select { //哪个通道可以操作就返回哪一个
case err := <-r.complete:
return err
case <-r.timeout:
return ErrTimeOut
}
}
package main
import (
common "awesomeProject2/Runner"
"log"
"os"
"time"
)
func main() {
log.Println("...开始执行任务...")
timeout := 3 * time.Second
r := common.New(timeout)
r.Add(createTask(), createTask(), createTask())
if err := r.Start(); err != nil {
switch err {
case common.ErrTimeOut:
log.Println(err)
os.Exit(1)//这个是代码超出了我们规定的时间,返回代码为1
case common.ErrInterrupt:
log.Println(err)
os.Exit(2)//这个时系统退出的代码,我们可以通过键盘发出
}
}
log.Println("...任务执行结束...")
}
func createTask() func(int) {
return func(id int) {
log.Printf("正在执行任务%d", id)
time.Sleep(time.Duration(id) * time.Second)
}
}
当你关闭一个通道时,所有还在等待接收数据的 goroutine 会收到一个零值和一个表示通道已关闭的标志。这允许接收方在没有额外同步的情况下知道通道已关闭。
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ready := make(chan struct{})
for i := 0; i < 3; i++ { //通过循环常见三个并发的运动员
wg.Add(1)
//表示每个运动员开始将waitGroup的计数加1
//表示有一个并发任务需要等待
go func(id int) {
defer wg.Done() //当运动员完成任务,将wg的计数减去
println(id, ":ready.") //输出运动员的准备状态
<-ready //运动员在这里阻塞,等待发令的信息
println(id, "running...")
}(i)
}
time.Sleep(time.Second) //主线程等待一秒
println("Ready?Go!") //输出发令口号
close(ready) //关闭通道,向所有运动员发送发令信号,接触阻塞状态
wg.Wait() //等待所有运动员(goroutine)完成任务
}
向已关闭通道发送数据,引发panic
从已关闭接收数据,返回已缓冲数据或零值 【The Valid】
无论收发,nil通道都会阻塞
package main
/*
向已关闭通道发送数据,引发panic
从已关闭接收数据,返回已缓冲数据或零值 【The Valid】
无论收发,nil通道都会阻塞
*/
func main() {
c := make(chan int, 3)
c <- 10
c <- 20
close(c)
for i := 0; i < cap(c)+1; i++ { //cap是通道设定的值,不管关闭与否,这个都不会改变
x, ok := <-c
println(i, ":", ok, x)
}
}
一旦我们设定了通道的方向,就不会再改变了
select语句
随机选择一个可用通道作为手法操作
2.8
反射
反射让哦我们能在运行期探知对象的类型信息和内存结构
反射操作所需的全部信息都源自接口变量。接口变量除了存储自身类型外,还会保存实际对象的类型数据
func TypeOf(i interface{}) Type
将任何传入的对象转化为接口类型
我们可以通过reflect类来获取实际对象获取类型
package main
import (
"fmt"
"reflect"
)
/*
面对类型的时候,需要区分kind和type。前者是底层类型,后者是真实类型(静态类型)
*/
type X int
type Y int
func main() {
var a, b X = 100, 200
var c Y = 300
ta, tb, tc := reflect.TypeOf(a), reflect.TypeOf(b), reflect.TypeOf(c)
fmt.Println(ta == tb, ta == tc)
fmt.Println(ta.Kind() == tc.Kind())
}
func main() {
a := reflect.ArrayOf(10, reflect.TypeOf(byte(0)))
m := reflect.MapOf(reflect.TypeOf(""), reflect.TypeOf(0))
fmt.Println(a, m)
}
//通过reflect来构建一些基础复合类型
方法Elem返回指针、数组、切片、字典或通道的基类型
2.9
package main
import (
"fmt"
"reflect"
)
type user struct {
name string
age int
}
type manager struct {
user //这就是我们的匿名字段
title string
}
/*
只有获取结构体指针的基类型后,才能遍历它的字段
基类型:reflect.Elem()
*/
func main() {
var m manager
t := reflect.TypeOf(&m) //使用反射获取m变量的类型信息,并将其存储在变量t
if t.Kind() == reflect.Ptr {
//获取指针的基类型
t = t.Elem()
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i) //获得结构体的第i个 字段信息
fmt.Println(f.Name, f.Type, f.Offset) //打印字段的名称,类型和偏移量
if f.Anonymous {
//如果访问的是匿名字段 --user
for x := 0; x < f.Type.NumField(); x++ {
af := f.Type.Field(x)
fmt.Println(" ", af.Name, af.Type)
}
}
}
}
package main
import (
"fmt"
"reflect"
)
type user struct {
name string
age int
}
type manager struct {
user //这就是我们的匿名字段
title string
}
/*
只有获取结构体指针的基类型后,才能遍历它的字段
基类型:reflect.Elem()
*/
func main() {
var m manager
t := reflect.TypeOf(m) //使用反射获取m变量的类型信息,并将其存储在变量t
name, _ := t.FieldByName("name")
//按名称寻找
fmt.Println(name.Name, name.Type)
age := t.FieldByIndex([]int{0, 1})
//按照多级索引查找
fmt.Println(age.Name, age.Type)
}
说一下互斥
如果一个数据永远也不会被修改,那么其实不存在资源竞争的问题.
所以互斥应该是读取和修改,修改和修改之间
2.16
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
// 使全部goroutine执行完的,类似于计数器的东西
func worker(ch <-chan bool) {
defer wg.Done()
LABLE:
for {
select {
case <-ch:
break LABLE
default:
fmt.Println("work...")
time.Sleep(time.Second)
}
} //从外部程序使这个gouroutine终止
/*
LABLE: 是一个标签(Label),它通常用于循环或 switch 语句中,允许在多层嵌套的控制流结构中引用特定的位置。
在你提供的代码中,LABLE: 标签用于 for 循环外部。
这个标签被命名为 LABLE,但它可以是任何有效的标识符。
当 select 语句中的某个 case 条件满足时,通过 break LABLE,循环将被中断,
并且执行会跳转到 LABLE 标签之后的代码。
*/
}
func main() {
exitChan := make(chan bool)
wg.Add(1)
exitChan <- true
go worker(exitChan)
//如果缓冲区没有,就一定需要另一边的管道来接收,否则阻塞之后,worker不能再接收信息,这段代码有错误
wg.Wait()
fmt.Println("over")
}
func worker(ch chan <-struct{}) {
defer wg.Done()
for {
fmt.Println("work...")
time.Sleep(time.Second)
if exit == true {
break
}
}
}//一般用struct作为参数,节省内存