Golang | 基础语法

本文章是根据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类型,一般是不使用的)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恒嘉宇

觉得好的话给小弟一点鼓励吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值