惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
V
Vulnerabilities – Threatpost
Security Latest
Security Latest
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
量子位
H
Help Net Security
Attack and Defense Labs
Attack and Defense Labs
The GitHub Blog
The GitHub Blog
L
LINUX DO - 最新话题
A
Arctic Wolf
博客园_首页
S
Securelist
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
酷 壳 – CoolShell
酷 壳 – CoolShell
Stack Overflow Blog
Stack Overflow Blog
N
Netflix TechBlog - Medium
Cyberwarzone
Cyberwarzone
小众软件
小众软件
T
Threatpost
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Blog — PlanetScale
Blog — PlanetScale
N
News and Events Feed by Topic
NISL@THU
NISL@THU
Forbes - Security
Forbes - Security
博客园 - 聂微东
F
Fortinet All Blogs
Simon Willison's Weblog
Simon Willison's Weblog
H
Heimdal Security Blog
罗磊的独立博客
S
Security @ Cisco Blogs
B
Blog
T
Troy Hunt's Blog
Engineering at Meta
Engineering at Meta
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
Hacker News - Newest:
Hacker News - Newest: "LLM"
I
Intezer
T
Threat Research - Cisco Blogs
C
Cybersecurity and Infrastructure Security Agency CISA
The Cloudflare Blog
S
Schneier on Security
月光博客
月光博客
L
LINUX DO - 热门话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org

Inno' Blog

将 what-digit-you-write 部署到 Heroku上 | Inno' Blog 论文浅析:GpSense: A GPU-friendy method for common-sense subgraph matching in massively parallel architectures Github项目clone速度过慢?试试这招 | Inno' Blog Hadoop分布式搭建全流程记录 | Inno' Blog Shell编程学习记录 | Inno' Blog 重学MySQL之视图 | Inno' Blog 重学MySQL之查询练习 | Inno' Blog 重学MySQL之事务 | Inno' Blog 重学MySQL之范式 | Inno' Blog 重学MySQL之基本操作和约束 | Inno' Blog 为你的Web应用提供HTTPS服务 | Inno' Blog 一个关于切片的小问题 | Inno' Blog 关于英译中的个人技巧 | Inno' Blog 不知不觉已经写够了十万行代码 | Inno' Blog YARN 架构 | Inno' Blog HDFS 架构 | Inno' Blog 正则中关于环视(lookaround)的小例子 | Inno' Blog 设计模式(十八):访问者模式 | Inno' Blog 如何借助Github进行协同开发 | Inno' Blog
Go语言学习笔记 | Inno' Blog
Inno Fang · 2020-02-11 · via Inno' Blog

Go 语言入门

变量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a int 
var b int = 10
var c, d int
var e, f int = 3, 4
var g, h, i, j = 5, 6, true, "def"
k, l, m := 7, 8.1, false, "def"

var (
aa = 1
bb = 2.2
cc = "define"
dd = true
)


TIPS 运行 GO 的 main 函数,所在包的包名也要是 main

1
2
3
4
5
6
7
package main  

import "fmt"

func main() {
fmt.Println("Hello World")
}

内建变量

  • bool 布尔型
  • string 字符串型
  • (u)int, (u)int8, (u)int16, (u)int32, (u)int64, (u)intptr 数字表示int位数,不加就默认为当前计算机位数,ptr 表示指针,u表示无符号
  • byte 字节型,8位1字节
  • rune Go语言的字符型,32位4个字节
  • float32, float64 浮点型,数字为位数
  • complex64, complex128 复数类型,前者实部和虚部都是 float32,后者两个都是 float64

强制类型转换

Go语言类型转换是强制

常量的定义

与变量的定义有很多相似的地方,不过要用 const 代替 var

定义常量的时候可以指定类型,也可以不指定类型,若不指定类型,就是个位置类型的文本,在使用的时候会进行推断

1
2
3
4
const a, b = 3, 4
var c = int
c = int(math.Sqrt(a * a + b * b))

定义枚举类型

Go 语言没有枚举值,只能用 const 来模拟

1
2
3
4
5
6
7
8
9
10
const (
Mon = iota
Tue
Wed
Thr
Fri
Sat
Sun
)
fmt.Println(Mon, Tue, Wed, Thr, Fri, Sat, Sun)

iota 是一个预先定义的标识符,在 const 表达式中(通常在括号内),用来表示当前 const 规范的非类型整数顺序数,索引为 0

条件语句

if

1
2
3
4
5
6
7
const filename = "in.txt"
contents, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%s\n", contents)
}

Go的条件语句的特点

  1. if 条件里可以赋值,且赋值的变量作用域只在这个 if 语句里
  2. 条件语句部分不需要括号,并且可以用 ; 来分隔多个条件

上面的例子可以进一步修改为

1
2
3
4
5
if contents, err := ioutil.ReadFile(filename); err == nil {
fmt.Printf("%s\n", contents)
} else {
fmt.Println(err)
}

switch

1
2
3
4
5
6
7
8
9
10
11
12
func eval(a, b int, op string) int {
var result int
switch op {
case "+": result = a + b
case "-": result = a - b
case "*": result = a * b
case "/": result = a / b
default:
panic("unsupported operator:" + op)
}
return result
}

Go 语言中 switch 的特点:

  1. swtich 会自动 break,除非使用 falllthrough
  2. switch 后可以没有表示,在 case 中写表达式也可以

循环语句

for

去其他语言的 for 循环的区别在于没有 ( ),并且三个部分各自都可以省略

1
2
3
4
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}

Go 语言中没有 while,如果把 for 玄幻的初始化和递增条件省略,就类似于 while

如果把三个部分都省略,那么就相当于死循环

1
2
3
for {
...
}

函数

多个返回值

Go 的函数可以返回多个值

1
2
3
func div(a, b int) (int, int) {
return a / b, a % b
}

此外,还可以给返回值命名,可以增强代码可读性,也可以为编辑器的补全提供帮助。

若指明了返回值名称,则可以对返回值进行直接赋值,最后只需要一个 return 即可(该用法仅用于非常简单的函数,否则影响可读性)

1
2
3
4
5
func div(a, b int) (q, r int) {
q = a / b
r = a % b
return
}

函数式编程

1
2
3
4
5
6
func apply(op func(int, int) int, a, b int) int {
fmt.Printf("Calling %s with (%d, %d)\n",
reflect.ValueOf(op).Pointer(runtime.FuncForPC(p).Name()),
a, b)
return op(a, b)
}

使用

1
2
3
apply(func(a int, b int) int {
return a * b
}, 2, 3)

可变参数列表

Go 语言没有默认参数函数重载,但有可变参数列表

1
2
3
4
5
6
7
func sum(numbers ...int) int {
s := 0
for i := range numbers {
s += numbers[i]
}
return s
}

指针

Go 语言的指针不能进行运算,这也是他简单的地方
Go 语言只有值传递一种方式(与值传递对应的是类似与 C++ 的引用传递,这个才 Go 中是没有的)

1
2
3
func swap(a, b *int) {
*a , *b = *b, *a
}

数组

1
2
3
4
var arr1 [5]int 			
arr2 := [3]int{1, 3, 5}
arr3 := [...]int{1, 3, 4}
var grid [3][4]int

数组的遍历,可以使用 range 关键字,可以获得下标和值

1
2
3
4
arr := [...]int{2, 4, 6, 8, 10}
for i, v := range arr {
fmt.Println(i, v)
}

如果只需要值不需要下标,则可以用 __ 代替

1
for _, v := range arr { ... }

若只要下标,则可以直接写成

1
for i range arr { ... }

Go 中的数组是值类型,也就是说数组传递给函数后,会拷贝数组内容,修改数组的值不会改变原数组的内容(大部分语言的数组都是引用传递,而 Go 则是值传递)。此外[10]int[20]int 是不同类型,也就是说实参数组的长度要与形参数组的长度相一致,否则会报错。

要实现引用传递,则需要用到指针

1
2
3
4
5
func foo(arr *[5]int) { ... }
func main() {
var arr[5]int
foo(&arr)
}

可见使用数组还是相当麻烦的,所以在 Go 中使用的更多的是切片(Slice)

切片

1
2
3
4
5
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
fmt.Println(arr[2:6])
fmt.Println(arr[:6])
fmt.Println(arr[2:])
fmt.Println(arr[:])

切片生成的是数组的一个视图,修改切片会改变原数组,在函数中传递数组时,若要方便修改原数组,可以使用切片

1
2
3
4
5
func foo(arr []int) { ... } 
func main() {
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
foo(arr[:])
}

  1. slice 可以向后扩展,但是不能向前扩展
  2. s[i] 不可以超越 len(s),向后扩展不可以超越底层数组 caps(s)
  3. 向slice中添加元素,如果长度没超过 cap,则会覆盖原数组的内容,若超过cap,系统则会重新分配更大的底层数组
  4. 由于值传递,使用 append 有可能会修改 slice 的 len 或 cap ,因此必须接受 append 的返回值,即 newSlc := append(slc, newValue)

切片的创建方式

1
2
3
s1 := []int{2, 4, 6, 8}		
s2 := make([]int, 16)
s3 := make([]int, 10, 32)

切片的操作

切片的复制

1
copy(s1, s2) 

切片元素的删除

1
2
3
4
5
6
7
s = append(s[:3], [4:]) 

front := s[0]
s = s[1:]

tail := s[len(s) - 1]
s = s[:len(s)-1]

Map

定义形式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
map[K]V
map[K1]map[K2]V


m := map[string]string {
"name" : "Inno",
"id" : "1234",
"gender" : "male",
}
fmt.Println(m)


m2 = make(map[string]string)
var m3 = map[string]string


for k, v := range m {
fmt.Println(k, v)
}


name := m["name"]
fmt.Println(name)


if age, ok := m["age"]; ok {
fmt.Println("age:", age)
} else {
fmt.Println("There is not a key called age!")

}


delete(m, "gender")


len(m)
  • map 使用哈希表,必须可以比较相等
  • 除了 slice、map、function 的内建类型都可以作为key
  • 自建类型(Struct 类型)也可以作为 key,前提是没有 slice、map、function

面向对象

Go 语言面向对象仅支持封装,不支持继承和多态;Go语言没有class,只有 struct

结构的创建和简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

type treeNode struct {
value int
left, right *treeNode
}


func createTreeNode(value int) *treeNode {
return &treeNode{value: value}
}

func main() {
var root treeNode



root = treeNode{value: 3}
root.left = &treeNode{}
root.right = &treeNode{5, nil, nil}
root.right.left = new(treeNode)
root.left.right = createTreeNode(2)


nodes := []treeNode {
{value: 3},
{},
{6, nil, nil},
}
fmt.Println(nodes)
}

Q 在工厂方法中返回的局部变量的地址,该局部变量是创建在堆上还是在栈上?

A 因为 Go 语言有垃圾回收机制,具体是在堆上还是在栈上,由编译器来决定。若是函数中的局部变量,那么建在栈上;若将局部变量的地址返回给外部调用,那么这个局部变量就会键在堆上,并参与垃圾回收,当不再使用时,就会被回收掉。所以退出函数后,局部变量就会被销毁,这是不一定的。

为结构定义方法

1
2
3
func (node treeNode) print() {
fmt.Print(node.value)
}
  • (node treeNode) 显示定义和命名方法接收者,随便叫什么都可以

因为在Go语言中都是值传递,那么如果要在函数内修改值的话,值传递是无法生效的,因此可以使用指针作为方法接收者

1
2
3
func (node *treeNode) setValue(value int) {
node.value = value
}
  • 只有使用指针才可以改变结构内容
  • nil 指针也可以调用方法

值接收者 VS 指针接收者

  1. 要改变内容时必须使用指针接收者
  2. 结构过大时也考虑使用指针接收者(因为值接收者是对值的拷贝,结果过大则消耗越大)
  3. 一致性:如果有指针接收者,最好都是指针接收者(一个建议,保持一致有利于阅读以及代码维护)
  4. 值接收者 是 Go 语言特有的
  5. 值/指针接收者均可接收值/指针

封装

名字一般使用CamelCase,首字母大写为 public,首字母小写为 private。

封装是对包而言的,每个目录可以有很多个包,但只能有一个main包,main 包包含可执行入口。

为结构定义的方法必须在同一个包内,但可以是不同文件。

扩充类型

  1. 使用别名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type Queue []int 


    func (q *Queue) Push(v int) {
    *q = append(*q, v)
    }

    func (q *Queue) Pop() int {
    head := (*q)[0]
    *q = (*q)[1:]
    return head
    }
  2. 使用组合

利用组合为之前的 treeNode对象扩展一个遍历方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

type myTreeNode struct {
node *treeNode
}

func (myNode *myTreeNode) postOrder() {
if myNode == nil && myNode.node == nil {
return
}
left := myTreeNode{myNode.node.left}
right := myTreeNode{myNode.node.right}

left.postOrder()
right.postOrder()
myNode.node.print()
}

func main() {
var root treeNode


myRoot := myTreeNode{&root}
myRoot.postOrder()
fmt.Println()
}

接口

接口的定义

使用者定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)


type Retriever interface {
Get(url string) string
}


func download(r Retriever) string {
return r.Get("https://www.baidu.com")
}

func main() {
var r Retriever

fmt.Println(download(r))
}

实现接口

在接口所在目录下新建目录,创建新文件用来实现接口方法,当前目录结构为

1
2
3
4
LearnGo>innofang.io>innofang>interfaces
>real
|- main.go // 接口实现所在位置
|- main.go // 接口定义所在位置

只要实现了接口方法,就被认为实现了接口,接口实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package real

import (
"time"
"net/http"
"net/http/httputil"
)

type Retriever struct {
UserAgent string
TimeOut time.Duration
}


func (r Retriever) Get(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}

result, err := httputil.DumpResponse(resp, true)
resp.Body.Close()

if err != nil {
panic(err)
}

return string(result)
}

使用接口实现

1
2
3
4
5
func main() {
var r Retriever
r = real.Retriever{}
fmt.Println(download(r))
}

接口里面有什么

接口变量里面的内容有两种情况:

  1. 实现者的类型和实现者的值
  2. 实现者的类型和实现者的指针,其中指针指向实现者

具体是哪个,可以自由选择,因此在使用接口变量的时候,不要使用接口的地址,因为接口内部包含有指针

  • 接口变量自带指针
  • 接口变量同样采用值传递,几乎不需要使用接口的指针
  • 指针接收者实现只能以指针方式使用;值接收者都可

若接口方法实现时使用了指针接收者,那么在定义该接口实习时,需要取接口地址

1
2
3
4

func (r *Retriever) Get(url string) string {
...
}

那么在定义该接口实现者时,就需要使用取地址符

1
2
3
4
5
func main() {
var r Retriever
r = &real.Retriever{}
fmt.Println(download(r))
}

查看接口变量

再定义一个接口实现者

1
2
3
4
5
6
7
8
9
10
package mock

type Retriever struct {
Contents string
}


func (r Retriever) Get(url string) string {
return r.Contents
}

查看接口变量有两种方式

Type Switch

1
2
3
4
5
6
switch v := r.(type) { 
case mock.Retriever:
fmt.Println("Contents: ", v.Contents)
case *real.Retriever:
fmt.Println("UserAgent: ", v.UserAgent)
}

Type Assertion

1
2
3
4
5
6
7
8
9
10
11
12
realRetriever := r.(*real.Retriever) 
fmt.Println(realRetriever.TimeOut)





if mockRetriever, ok := r.(mock.Retriever); ok {
fmt.Println(mockRetriever.Contents)
} else {
fmt.Println("not a mock")
}

表示任何类型interface{}

1
2
3
4
5
6
7
8
9
10
11
type Quue []interface{} 

func (q *Queue) Push(v interface{}) {
*q = append(*q, v)
}

func (q *Queue) Pop() interface{} {
head := (*q)[0]
*q = (*q)][1:]
return head
}

接口的组合

假设有另一个结构体 Poster,现在要把含有 Get 方法的 Retriever 和含有 Post 方法的 Poster 进行组合

1
2
3
4
5
func RetrieverPoster interface {
Retriever
Poster

}

现在对于实现者来说,他可以自己实现 Post 方法和 Get 方法了

使用者不管定义者如何定义,它只管实现它要的方法,不需要关心使用的是什么接口

只要是一个类型就可以实现接口,这也是 Go 语言灵活的地方

defer 调用

何时使用 defer 调用

  • Open/Close
  • Lock/Unlock
  • PrintHeader/PrintFooter

被 defer 修饰的语句会确保在函数结束时发生,无论这个程序是否被提前终止,如果有多个 defer 会有一个栈来存储语句,采用先进后出的顺序执行语句

错误处理

1
2
3
4
5
6
7
8
9
file, err := os.Open("abc.txt")
if err != nil {

if pathError, ok := err.(*os.PathError); !ok {
panic(err)
} else {
fmt.Println("%s, %s, %s\n", pathError.Op, pathError.Path, pathError.Err)
}
}

panic

  • 停止当前函数执行
  • 一直向上返回,执行每一层的 defer
  • 如果没有遇见 recover,程序退出

recover

  • 仅在 defer 调用中使用
  • 获取 panic 的值
  • 如果无法处理,可重新 0panic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"errors"
"fmt"
)

func tryRecover() {
defer func() {
r := recover()
if err, ok := r.(error); ok {

fmt.Println("Error occured:", err)
} else {

panic(fmt.Sprint("I don't know what to do: %v", r))
}
}()
panic(errors.New("this is an error"))
}

func main() {
tryRecover()
}

表格驱动测试

  • 分离的测试数据和测试逻辑
  • 明确的出错信息
  • 可以部分失败
  • Go 语言的语法使得我们更易实践表格驱动测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Test(t *testing.T) {
tests := []struct {
a, b, c int32
}{
{1, 2, 3},
{0, 2, 2},
{0, 0, 0},
{-1, 1, 0},
{math.MaxInt32, 1, math.MinInt32},
}

for _, test := range tests {
if actual := add(test.a, test.b); actual != test.c {

...
}
}
}

代码覆盖率

在测试文件所在目录下执行

1
2
go test -coverprofile=c.out 
go tool cover -html=c.out

性能测试

1
2
3
4
5
6
7
8
9
10
11
func Becnchmark(b *testing.B) {
testCase := ...
ans := ...

for i := 0; i < b.N; i++ {
actual := testedFunc(testCase)
if actual != ans {
b.Errorf("...", ...)
}
}
}

命令行在测试文件所在目录下执行

1
go test -bench .

性能调优

在测试文件所在目录下执行

1
go test -bench . -cpuprofile cpu.out

生成了二进制文件 cpu.out,接着使用命令

1
go tool pprof cpu.out

进入交互式命令行,可以使用 help 查看,最简单的方式是输入 web 查看二进制文件。内容上,看到的方框越大,说明耗时越久,优化方框大的部分可以使程序性能更有。

Goroutine

协程

  • 轻量级“线程”
  • 非抢占式多任务处理,由协程序主动交出控制权
  • 编译器/解释器/虚拟机层面的多任务
  • 多个协程可能在一个或多个线程上运行

因为是非抢占式的 ,若不切换协程,则会一直执行一个协程(io操作会自动切换协程)。因此对于自定义协程,需要注意有没有手动交出控制权

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var a [10]int
for i := 0; i < 10; i++ {
go func(i int) {
for {
a[i]++
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
}

goroutine 的定义

  • 任何函数只需要加上 go 就能送给调度器运行
  • 不需要在定义时区分是否是异步函数
  • 调度器在合适的点进行切换
  • 调试时,在命令行使用 -race 来检测数据访问冲突 go run -race goroutine.go

goroutine 可能的切换点

  • I/O, select
  • channel
  • 等待锁
  • 函数调用(有时)
  • runtime.Gosched()

只是参考,不能保证切换,不能保证在其他地方不切换

通道

指的是 goroutine 之间的通道,可以让 goroutine 之间互相通信。每个通道都有与其相关的类型,即通道允许传输的数据类型。通道的零值为 nil,需要使用内置 make 方法来定义

1
2
3
4

var 通道名 chan 数据类型

通道名 = make(chan 数据类型)

通道的注意点

Channel 通道在使用的时候,有以下几个注意点:

  1. 用于 goroutine,传递消息
  2. 通道,每个都有相关联的数据类型,nil chan 是无法使用的
  3. 使用通道传递数据:<-
    1. chan <- data,发送数据到通道。向通道中写
    2. data <- chan,从通道中获取数据。从通道中读数据
  4. 阻塞
    1. 发送数据:chan <- data,阻塞,直到另一条 goroutine 读取数据来接触阻塞
    2. 读取数据:data <- chan,阻塞,知道另一条 goroutine 写入数据来接触阻塞
  5. Channel 是同步的,意味着同一时间只有一条 goroutine 来操作

通道是 goroutine 之间的连接,所以通道的发送和接收必须处在不同的 goroutine 中

1
2
data := <- a 
a <- data

定义方式

1
2
3
4
5
6
7
8
9
10
11

c := make(chan int)
go func() {
for {
n := <- c
fmt.Println(n)
}
}()
c <- 1
c <- 2
time.Sleep(time.Millisecond)

关闭 channel

channel 可以被发送方 close,但是 channel 即使被 close 了,也会接收数据,只不过收到的是 zeroValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func worker(id int, c chan int) {
for {










for n := range c {
fmt.Printf("worker %d received %c\n", id, n)
}
}
}


func channelClose() {
c := make(chan int, 3)
go worker(0, c)
c <- 'a'
c <- 'b'
c <- 'c'
c <- 'd'
close(c)
time.Sleep(time.Millisecond)
}

Select

select 是 Go 中的一个控制结构,select 语句类似于 switch 语句,但是 select 会随机执行一个可执行的 case。如果没有 case 可运行,若设置了 default 则会执行 default,否则它将阻塞,直到有 case 可执行。语法结构上与 switch 语句很相似

1
2
3
4
5
6
7
8
select {
case communication clause:
statement(s)
case communication clause:
statement(s)
default:
statement(s)
}
  • 每个 case 都必须是一个通信
  • 所有 channel 表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行
  • 若此刻没有 case 语句可以执行时,若有default语句,则执行该语句;否则 select 将阻塞,知道有某个通信可以运行,Go 不会重新对 channel 或值进行求值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
ch1 := make(chan int)
ch2 := make(chan int)

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

select {
case num1 := <-ch1:
fmt.Println("Read from ch1:", num1)
case num2, ok := <-ch2:
if ok {
fmt.Println("Read from ch2:", num2)
} else {
fmt.Println("ch2 has been closed")
}
case <-time.After(1 * time.Second):
fmt.Println("After 3 seconds")
default:
fmt.Println("default ...")
}
}