Golang 基础之基础语法梳理 (二)

大家好,今天将梳理出的 Go语言基础语法内容,分享给大家。 请多多指教,谢谢。

本次《Go语言基础语法内容》共分为三个章节,本文为第二章节

本章节内容

  • channel
  • 结构体
  • 指针
  • 控制语句

channel

介绍

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义,channel就是它们之间的连接。

channel可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

注意:goroutine 是go语言中特有的机制,可以理解为go语言中的线程。 使用goroutine, 可以使用go关键字

go并发方面的知识会放到后续文章中,和大家分享。 此知识点中简单了解即可

使用

channel 声明

channel是一种类型,一种引用类型。语法格式:

var 变量 chan 元素类型

例子

 var ch1 chan int   // 声明一个传递整型的通道
 var ch2 chan bool  // 声明一个传递布尔型的通道
 var ch3 chan []int // 声明一个传递int切片的通道    

创建 channel

通道是引用类型,通道类型的空值是nil。

声明的通道后需要使用make函数初始化之后才能使用。

make(chan 元素类型, 缓冲大小) // 格式

例子

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用 <- 符号。

发送

将一个值发送到通道中

ch := make(chan int)
ch <- 100 // 把100发送到 ch 中

接收

从一个通道中接收值

x := <- ch // 从ch通道中接收, 并赋值 x
<- ch // 从ch通道中接收, 忽略值 

关闭

调用内建 close 函数来关闭通道

close(ch)

关闭后的通道有以下特点:

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

需要注意:只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

无缓冲通道

无缓冲的通道又称为阻塞的通道

无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

使用 ch := make(chan int) 创建的是无缓冲的通道

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}   

上面这段代码能够通过编译,但是执行的时候会出现死锁错误

fatal error: all goroutines are asleep - deadlock!

一种方法是启用一个 goroutine 去接收值

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}   

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲通道

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

使用 make函数 初始化通道的时候为其指定通道的容量

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

从通道循环取值案例

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}   

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

单向通道案例

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}  

其中:

  1. chan<- int 是一个只能发送的通道,可以发送但是不能接收;
  2. <-chan int 是一个只能接收的通道,可以接收但是不能发送。

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

指针

介绍

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。

传递数据使用指针,而无须拷贝数据。

Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。

Go语言中的值类型 (int、float、bool、string、array、struct) 都有对应的指针类型,如:*int、*int64、*string等。

当一个指针被定义后没有分配到任何变量时,它的值为 nil

使用

取变量指针的语法如下

var v int = 10
ptr := &v
fmt.Printf("v:%d ptr:%p\n", v, ptr)

c := *ptr // 指针取值(根据指针去内存取值)
fmt.Printf("c:%d\n", c)

v: 代表被取地址的变量,ptr: 用于接收地址的变量,ptr的类型称做int的指针类型。*代表指针。

程序定义一个int变量num的地址并打印

将a的地址赋给指针p,并通过p去修改a的值

func main() {
    var a int
    fmt.Println(&a) // 指针地址
    var p *int
    p = &a  // 等同于 p := &a
    *p = 20
    fmt.Println(a) // 输出 20
}

空指针的判断

func main() {
    var p *string
    fmt.Printf("p的值是%v\n", p)
    if p != nil {
        fmt.Println("非空")
    } else {
        fmt.Println("空值")
    }
}

结构体

介绍

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体,每个值称为结构体的成员。

通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行。

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。 一个结构体可能同时包含导出和未导出的成员。

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。

使用

type Info struct {  // 创建结构体
    ID	  int
    Name, Hobby  string
}
var info Info // 声明结构体

结构体变量的成员可以通过点操作符访问

info.Name = "帽儿山的枪手"

对成员取地址,然后通过指针访问

name := &info.Name
fmt.Println(*name)

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

func UpdateInfo(i *info) {
    i.Hobby = "旅游"
}

通过指针创建并初始化一个结构体变量,并返回结构体的地址

pp := &Point{1, 2}

它和下面的语句是等价的

pp := new(Point)
*pp = Point{1, 2}

注:结构体也是可以比较的,两个结构体将可以使用或!=运算符进行比较。相等比较运算符将比较两个结构体的每个成员。

控制语句

if 判断

单条件判断

if condition {
    // do something
}

多条件判断

if condition {
    
} else if condition {
    // do something
} else {
    // do something
}

if 单条件先跟个语句然后再做条件判断

if statement;condition{
    //do something
}

多条件带语句判断

if num := 78;num <= 50{
    fmt.Println("Number is less then 50")
} else if num >= 51 && num <= 100{
    fmt.Println("The number is between 51 and1 100")
} else{
    fmt.Println("The number is greater than 100")
}

for 循环

go语言中只有一种循环方式,for循环。

第一种语法格式

for 循环变量初始化;循环条件;循环变量迭代 {
	// 循环操作(语句)
}

for j := 1; j <= 10; j++ {
    // 循环执行语句
}

第二种语法格式

for 循环判断条件 {
    // 循环执行语句
}

j := 1
for j <= 10 {
    j++
}

第三种语法格式

for {
    // 循环执行语句,是一个无限循环,通常需要配合 break 语句使用
}

遍历式语法格式 for-range

var names []string{"xiaoming", "xiaohong", "xiaojun"}
for _, name := range names {
    fmt.Println(name)
}

使用 goto 语句跳出 for 循环

func main() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
        if i == 5 {
            goto end
        }
    }
    end:
        fmt.Println("end")
}

输出

0
1
2
3
4
5
end

switch

第一种语法格式

func main() {
    num := 1
    switch num {
        case 1:
        	fmt.Println("num=1")
        case 2:
        	fmt.Println("num=2")
        case 3:
        	fmt.Println("num=3")
        default:
        	fmt.Println("未匹配")
    }
}

第二种语法格式

func main() {
    num := 1
    switch {
        case num == 1:
        	fmt.Println("num=1")
        case num == 2:
        	fmt.Println("num=2")
        case num == 3:
        	fmt.Println("num=3")
        default:
        	fmt.Println("未匹配")
    }
}

第三种语法格式

func main() {
    switch num := 1; {
        case num == 1:
        	fmt.Println("num=1")
        case num == 2:
        	fmt.Println("num=2")
        case num == 3:
        	fmt.Println("num=3")
        default:
        	fmt.Println("未匹配")
    }
}

第四种语法格式

使用关键字 fallthrough 。默认在switch中,每个case都会有一个隐藏的break,如果想要去掉隐藏的break,我们就可以使用fallthrough来进行取代

package main

import (
    "fmt"
)

func main() {
    a := 2
    switch a {
    case 1:
        fmt.Println("a=1")
    case 2:
        fmt.Println("a=2")
        fallthrough
    case 3:
        fmt.Println("a=3")
        case 4:
        fmt.Println("a=4")
    default:
        fmt.Println("default")
    }
}

输出

a=2
a=3

select

select语句用来处理与channel有关的I/O操作:

  1. 每个case都必须是一个通信;
  2. 所有channel表达式和被发送的表达式都会被求值;
  3. 任意某个通道可以运行,它就执行,其他被忽略;
  4. 多个case可以运行,随机选一个执行;
  5. 都不可以运行,有default,执行default,没有就阻塞,直到某个通信可以运行,且不会重新对表达式求值;
  6. 一个select最多执行一次case里的代码,需要一直检测case,外层加for循环;
  7. case里的break只退出当前select,和for循环无关;

随机执行case用法

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        time.Sleep(time.Second)
        ch1 <- 1
    }()

    go func() {
        time.Sleep(time.Second)
        ch2 <- 2
    }()

    time.Sleep(2 * time.Second)
    select {
    case i := <-ch1:
        fmt.Println("ch1 receive", i)
    case i := <-ch2:
        fmt.Println("ch2 receive", i)
    default:
        fmt.Println("no i/o opeartion")
    }
}

结果随机打印:ch1 receive: 1ch2 receive: 2,因为两个channal等待写入,select里两个case都符合执行条件,随机执行。

设置接收channel超时用法

func main() {
    ch1 := make(chan int)
    select {
    case i := <-ch1:
        fmt.Println(i)
    case <-time.After(5 * time.Second):
        fmt.Println("ch receive timeout")
    }
}

检查 channel 是否满了用法

func main() {
    ch1 := make(chan int, 5)
    ch1 <- 1
    ch1 <- 2
    ch1 <- 3
    ch1 <- 4
    ch1 <- 5

    select {
    case ch1 <- 6:
        fmt.Println("send 6 to ch1")
    default:
        fmt.Println("channel is full")
    }
}

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号【 帽儿山的枪手 】,关注我

posted @ 2022-03-20 00:30  帽儿山的枪手  阅读(31)  评论(0编辑  收藏  举报