本文章是根据b站up主:@刘丹冰Aceld 的视频《8小时转职Golang工程师》学习后自己所总结的笔记,想看视频的请移步:《8小时转职Golang工程师》
文章目录
main 函数初见 Golang 语法注意点
package main //这是程序包名
import "fmt" //记得导入程序所需要的包,但切记不要导入冗余包
func main(){ //程序的main函数,也是程序的主入口
fmt.Println("Hello Golang!")
}
可用 go run hello.go
来执行此go文件
go run
:编译并执行
go build
:编译go文件,生成可执行文件
golang中的表达式或者是代码都可以加或不加分号,但是为了美观建议不加
import导包的时候可以使用多条import语句依次一个一个地把包导入,也可以使用以下形式一次性导入所需的包
import(
"fmt"
"time"
)
注意:golang严格要求左右括号的位置!左括号一定是跟在当前函数名或语句的同一个位置中,这是golang对代码风格的一种强制
Golang 中4种变量的声明方式
// 1
var a int
// 未给初始化的变量赋值,会给这种变量默认赋值为0
// 2
var a int = 10
// 初始化变量,并且为其赋值
// 3
var a = 100
// 在初始化的时候,省去数据类型,通过值自动匹配当前变量的数据类型
// 4
a := 1000
// 最常用的一种方式,省去var关键字,直接自动匹配
// 但是需要注意一点:在声明全局变量的时候,前三种声明方式都可以,唯独第4种不行
// := 这种方式只能够用在函数体内
声明多个变量
var x, y int = 100, 200
var x, y = 10, "golang"
var (
a int = 100
b bool = true
)
fmt.Printf()
表示格式化输出,可以用 %T
占位符来输出其类型,可以用 %v
占位符来输出其所有详细信息
var a string = "golang"
slice1 := []int{1, 2, 3}
fmt.Printf("type of a = %T", a)
fmt.Printf("len = %d, slice = %v\n", len(slice1), slice1)
const 与 iota
const 关键字可以声明常量(常量具有只读属性)
const a int = 100
a = 10 //报错!常量是不允许修改的
const(
BEIJING = 1
SHANGHAI = 2
SHENZHEN = 3
GUANGZHOU = 4
)
golang中可以在const()中添加一个关键字iota,这样每行的iota都会累加1且第一行的iota默认值为0,而且除第一行外,若以下的常量未表明表达式,默认和第一行的表达式一致
const(
BEIJING = 10 * iota //0
SHANGHAI //10
SHENZHEN //20
GUANGZHOU //30
)
var a int = iota //报错!iota只能够配合const()一起使用!iota只能在const里才能够累加!
函数的三种返回形式
func foo1(a string, b int) int {
//返回单个返回值
fmt.Printf("a = ", a)
fmt.Printf("b = ", b)
c := 100
return c
}
func foo2(a string, b int) (int, int) {
fmt.Printf("a = ", a)
fmt.Printf("b = ", b)
return 6, 9
}
func foo3(a string, b int) (r1 int, r2 int) {
//初始化默认值为0,给有名称的返回值变量赋值
fmt.Printf("a = ", a)
fmt.Printf("b = ", b)
r1 = 1
r2 = 2
return
}
func foo4(a string, b int) (r1, r2 int) {
//返回值是相同类型
fmt.Printf("a = ", a)
fmt.Printf("b = ", b)
r1 = 1
r2 = 2
return
}
import 导包以及 init 方法调用流程
如果一个函数名的首字母大写的话代表当前函数对外开放,如果小写则表明当前函数只能在包内调用
如果一个包一旦被导入但任意的接口都未被使用就会报错 imported but not used
import 匿名以及别名导包
实际开发环境中可能会有以下需求:
① 不需要调用包内所定义的接口
② 但是需要调用包内的init()方法
这就需要匿名导入了,可以在导入包的时候在包名的前面加一个下划线和空格 "_ "
import(
_ "lib1" //匿名导入
mylib "lib2" //给包起一个别名
. "lib3"
)
使用 “.” 这种方式的导入可以直接使用接口而无需lib3.test(),但要注意函数重名问题
defer 语句调用顺序
defer关键字会在当前所在的函数体结束之前触发,如若有多个defer语句,以压栈退栈的顺序执行,先进后出
func main(){
defer fmt.Printf("A")
defer fmt.Printf("B")
defer fmt.Printf("C")
defer fmt.Printf("-")
} //输出顺序为“ - C B A ”
defer 和 return 的先后顺序问题
func deferFunc() int {
fmt.Println("defer function called ...")
return 0
}
func returnFunc() int {
fmt.Println("return function called ... ")
return 0
}
func returnAndDeferFunc() int {
defer deferFunc()
return returnFunc()
}
func main() int {
returnAndDeferFunc()
//输出结果:
//return function called ...
//defer function called ...
}
defer是当前函数的生命周期全部结束之后,才会出栈执行,可以相当于当前代码执行到右括号" } "那里,也就是在return之后,因为return出去后才算这个函数的生命周期结束,所以是先return在defer
数组与动态数组
slice 切片,实际上是一种动态数组的类型
//定义固定长度的数组
var myArray1 [10]int
myArray2 := [10]int{1, 2, 3, 4}
myArray3 := [4]int{11, 12, 13, 14}
for i := 0; i < len(myArray1); i++{
//遍历数组
fmt.Println(myArray1[i])
}
for index, value := range myArray2 {
//遍历数组
fmt.Println(myArray2[i])
}
数组根据不同的长度,会有不同的数据类型
myArray1的数据类型是[10]int
myArray2的数据类型是[10]int
myArray3的数据类型是[4]int
所以这里会涉及一个问题,面对长度不同的数组,他们的数据类型是不同的,那么我们在函数调用的时候怎么进行传参呢?
func printArray(myArray [10]int) {
//这里的问题在于,固定死了数组的类型,对于其他类型的数组不适用,会报错
//仍然是一个值传递的拷贝过程,在函数里对数组的修改并不影响实际的数组
for index, value := range myArray {
//遍历数组
fmt.Println("index = ", index, ", value = ", value)
}
myArray[0] = 111 //值拷贝,修改不起作用
}
可以用切片/动态数组的方式解决这个问题
myArray := []int{1, 2, 3, 4}
//此时myArray的数组类型是:切片/动态数组类型:[]int
func printArray(myArray []int) {
// 切片/动态数组是一个引用传递,理解为当前数组的一个指针
// _ 表示匿名的变量
for _, value := range myArray {
//遍历数组
fmt.Println(myArray[i])
}
myArray[0] = 111 //引用传递,修改有效
}
动态数组本身就是一个指向数组本身内存的一块指针或引用
slice 切片的4种声明定义方式
// 1
//声明 slice1 是一个切片并且初始化,默认值是1,2,3,长度 len = 3
slice1 := []int{1, 2, 3}
fmt.Printf("len = %d, slice = %v\n", len(slice1), slice1) // len = 3, slice = [1, 2, 3]
// 2
//声明 slice2 是一个切片,但是并没有给 slice2 分配空间
var slice2 []int
fmt.Printf("len = %d, slice = %v\n", len(slice2), slice2) // len = 0, slice = []
slice2[0] = 1 // 报错,此时还未开辟空间
//开辟空间
slice2 = make([]int, 3) //开辟3个指定类型的空间,默认值为0
fmt.Printf("len = %d, slice = %v\n", len(slice2), slice2) // len = 3, slice = [0, 0, 0]
slice2[0] = 1 // 赋值成功
// 3
//声明 slice3 是一个切片,同时给 slice3 分配空间,3个空间,初始化值是0
var slice3 []int = make([]int, 3)
// 4
//声明 slice4,同时给 slice4 分配空间,3个空间,初始化值是0,通过 := 推导出 slice4 是一个切片
slice4 := make([]int, 3)
//判断一个 slice 是否为空
if slice == nil {
fmt.Println("slice 是一个空切片!")
} else {
fmt.Println("slice 是有空间的!")
}
slice 切片容量的追加以及 slice 切片的截取
在这里插入代码片
goroutine
可以用 go 关键字来创建一个 goroutine
package main
import (
"fmt"
"time"
)
//子goroutine
func newTask() {
i := 0
for {
i++
fmt.Println("new Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
//主goroutine
func main () {
//创建一个go程,去执行newTask()流程
//注意这里,如果main goroutine退出了,这个newTask的子goroutine会一同退出
go newTask()
i := 0
for {
i++
fmt.Println("main goroutine: i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
package main
import (
"fmt"
"time"
"runtime"
)
func main () {
//匿名go程
//用go创建承载一个形参为空,返回值为空的一个函数
go func () {
defer fmt.Println("A.defer")
func () {
defer fmt.Println("B.defer")
//如果想在当前子函数中退出当前go程,用return不行,return只会退出当前子函数
//可以用runtime.Goexit()
runtime.Goexit()
fmt.Println("B")
}
fmt.Println("A")
}() //这个括号表示调用匿名函数
//匿名go程
//用go创建承载一个有形参有返回值的函数
//注意这里,如何得到我们这个go程的返回值呢?
//go程在执行结束后并不会把函数的返回值给抛给函数的上一层
//它们本来就是并行的,互相操作到哪一步都是异步操作的
//如果此时用一个变量来接收返回值并不会执行阻塞操作,可以用管道channel机制来实现goroutine之间的信息传递
go func (a int, b int) bool {
fmt.Println("a = ", a, ", b = ", b)
return true
}(10, 20)
//死循环
for {
time.Sleep(1 * time.Second)
}
}
channel
- channel本身就是一种内置的数据类型
- 可以通过make来实现创建channel的初始化动作
make (chan Type) //无缓冲区,或者说缓冲区大小为0,等价于make (chan Type, 0)
make (chan Type, capacity) //缓冲区大小为capacity
- 那怎么向channel中写数据或者从channel中读取数据呢?
channel <- value //发送value到channel
<- channel //接收并将其丢弃
x := <- channel //从channel中接收数据并赋值给x
x, ok := <- channel //功能同上,同时检查通道是否已经关闭或者是否为空,ok表示是否读成功
package main
import "fmt"
func main () {
//定义一个channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine 结束。")
fmt.Println("goroutine 正在运行...")
c <- 666 //将666发送给channel c
}()
num := <- c
fmt.Println("num = ", num)
fmt.Println("main goroutine 结束...")
}
- 为什么这里每次num都能被打印出来呢?
- 因为这里channel是可以同步两个不同goroutine之间的能力的
- 如果当前main go先执行到了
num := <- c
这里,而我的sub go还没执行到c <- 666
,此时main go会发生一个阻塞,直到sub go中把666发给c,写到管道中,则阻塞消失,两者都正常运行 - 如果当前sub go先执行到了
c <- 666
这里,而我的main go还没执行到num := <- c
,此时sub go会发生一个阻塞,直到main go中把c中的666给读出来,从管道中读取,则阻塞消失,两者都正常运行
无缓冲channel
- 第一步:两个goroutine都到达通道,但哪个都没有开始执行发送或者接收
- 第二步:左侧的goroutine将它的手伸进了通道,这模拟了向通道发送数据的行为,这时这个goroutine会在通道中被锁住,直到交换完成
- 第三步:右侧的goroutine将它的手放入通道,这模拟了从通道里接收数据,这个goroutine一样也会在通道中被锁住,直到交换完成
- 第四步和第五步:进行交换
- 第六步:最终,两个goroutine都将它们的手从通道里面拿出来,这模拟了被锁住的goroutine得到释放,两个goroutine现在都可以去做其他事情了
有缓冲channel
- 第一步:右侧的goroutine正在从通道接受一个值
- 第二步:右侧的这个goroutine独立完成了接收值的动作,而左侧的goroutine正在发送一个新的值到通道里
- 第三步:左侧的goroutine还在向通道发送新值,而右侧的goroutine正在从通道接收另外一个值,这个步骤里两个操作既不是同步的也不会相互阻塞
- 第四步:最后,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) //带有缓冲的channel
fmt.Println("len(c) = ", len(c), ", cap(c) = ", cap(c))
go func() {
defer fmt.Println("子go程结束。")
for i := 0; i < 3; i++ { //思考这里改为4的情况
c <- i
fmt.Println("子go程正在运行...发送的元素:", i, " len(c) = ", len(c), ", cap(c) = ", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <- c //从c中接收数据并赋值给num
fmt.Println("num = ", num)
}
fmt.Println("main 结束。")
}
- 当
channel
已经满了,再向里面写数据会阻塞 - 当
channel
已经空了,再从里面读数据会阻塞
关闭channel
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close()可以关闭一个channel
//如果此行注释掉,编译器会报错:fatal error: all goroutines are asleep - deadlock!
close(c)
//向一个关闭的channel中添加数据,会报错panic
//panic: send on closed channel
c <- 666
}
for {
//ok如果为true则表示channel没有关闭,如果为false则表示channel已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main finished.")
}
channel
不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range
循环之类才需要去关闭channel
- 关闭
channel
后,无法向channel
再发送数据(引发panic
错误后导致接收立即返回零值) - 关闭
channel
后,可以继续从channel
中接收数据,直到数据读完了ok
才会为false
- 对于
nil channel
,无论收发都会被阻塞(也就是c
没有通过make
,而是直接定义一个channel
类型,一般是不使用的)