目录

  • 1.Channel基础
    • 为什么要引入Channel ?
    • Channel简介
    • channel的定义/声明
    • channel的数据类型:引用类型
    • 向channel中写入数据
    • 从channel中读取数据
  • 2 channel的遍历与关闭
    • channel的关闭
      • 使用内置函数close关闭channel
      • 向已关闭的channel中继续写入数据
    • channel的遍历
      • channel遍历的方式
        • 1.使用 range 关键字遍历 channel
        • 2.使用 for 循环和 select 语句遍历 channel
        • 3 使用 len 函数和 for 循环遍历 channel(难点)
          • 1) i < len(intChan) 陷入死循环
          • 解决方案1:使用cap(intChan)函数获取channel的容量
          • 2) 只等读到一半的数据
          • 解决方案2:for len(ch) > 0 {}
      • 遍历时,如果channel没有关闭,则会出现deadlock的错误
  • 3 实例演示:定义map型channel,并进行数据的读写
    • map型channel
    • map型channel2:
    • Struct型channel
    • 通过空接口实现存放任意数据类型的channel
    • 空接口型channel与类型断言

1.Channel基础

为什么要引入Channel ?

  • 使用全局变量加锁同步可以来解决goroutine的通讯,但不完美,原因如下:
  • 主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,只能估算。如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁。通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写操作。所以需要一个新的通讯机制来实现自动实现goroutine之间的通讯,主线程可以自动捕获goroutine结束,channel即可满足这种需求

Channel简介

  1. channle本质就是一个数据结构-队列
  2. 数据是先进先出【FIFO:first in first out】
  3. 线程安全,多goroutine 访问时,不需要加锁,就是说channel 本身就是线程安全的
  4. channel有类型的,一个string的channel只能存放 string类型数据。

channel的定义/声明

var 变量名 chan 变量类型

//channel是有类型的intChan 只能写入整数int
var intchan chan int
//mapChan用于存放map[int]string类型
var mapchan chan map[int]string

channel 必须初始化才能写入数据,即make后才能使用

//创建一个可以存放3个int类型的管道
intChan = make(chan int, 3)

声明的同时初始化

// 创建容量为 10 的 channelmapchan := make(chan map[string]string, 10)

channel的数据类型:引用类型

//看看intChan是什么fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
intChan 的值=0xc000108080 intChan本身的地址=0xc00000a028
//intchan 指向存放数据的地址

向channel中写入数据

int型

// 向管道写入数据intChan<- 10num := 211intChan<- numintChan<- 50// 如果从channel取出数据后,可以继续放入<-intChanintChan<- 98//注意点, 当我们给管道中写入数据时,不能超过其容量,数据放满后就不能再放入了

从channel中读取数据

   //从管道中读取数据var num2 intnum2 = <-intChanfmt.Println("num2=", num2)fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3num3 := <-intChannum4 := <-intChanfmt.Println("num3=", num3, "num4=", num4)  // num3= 50 num4= 98  // 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock//num5 := <-intChan// fmt.Println( "num5=", num5)//fatal error: all goroutines are asleep - deadlock!

2 channel的遍历与关闭

channel的关闭

使用内置函数close关闭channel

使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然
可以从该channel读取数据。
在 Go 中,我们可以使用内置函数 close 来关闭一个 channel。关闭一个 channel 后,就不能再向该 channel 中写入数据了,但仍然可以从该 channel 中读取数据。当从一个已经关闭的 channel 中读取数据时,会得到该 channel 类型的零值。

一个如何关闭channel的例子

package mainimport "fmt"func main() {// 创建一个容量为 10 的整数类型的 channelintChan := make(chan int, 10)// 向 channel 中写入 5 个整数for i := 1; i <= 5; i++ {intChan <- i}// 关闭 channelclose(intChan)// 从 channel 中读取所有整数for {// 从 channel 中读取一个整数value, ok := <-intChan// 如果 channel 已经关闭且里面没有数据了,就退出循环if !ok {break}// 输出读取的整数fmt.Printf("Read a value from intChan: %d\n", value)}// 再次从 channel 中读取数据,会得到零值value, ok := <-intChanfmt.Printf("Read a value from intChan after close: %d, %v\n", value, ok)
}

在这段代码中,我们创建了一个容量为 10 的整数类型的 channel intChan。然后我们向 intChan channel 中写入了 5 个整数。接着,我们使用 close 函数关闭了 intChan channel。最后,我们使用一个循环从 intChan channel 中读取所有整数,并将它们输出。在读取完所有整数后,我们再次从 intChan channel 中读取数据,这时会得到一个整数类型的零值和一个表示 channel 是否关闭的布尔值 false。

输出结果如下:

Read a value from intChan: 1
Read a value from intChan: 2
Read a value from intChan: 3
Read a value from intChan: 4
Read a value from intChan: 5
Read a value from intChan after close: 0, false

向已关闭的channel中继续写入数据

在 Go 中,当一个 channel 被关闭后,就不能再向该 channel 中写入数据了,否则会导致 panic。

下面是一个演示如何关闭 channel 的例子,同时在关闭后向该 channel 写入数据:

package mainimport "fmt"func main() {// 创建一个容量为 10 的整数类型的 channelintChan := make(chan int, 10)// 向 channel 中写入 5 个整数for i := 1; i <= 5; i++ {intChan <- i}// 关闭 channelclose(intChan)// 向已关闭的 channel 写入数据,会导致 panicintChan <- 6fmt.Println("write data to closed channel")// 从 channel 中读取所有整数for {// 从 channel 中读取一个整数value, ok := <-intChan// 如果 channel 已经关闭且里面没有数据了,就退出循环if !ok {break}// 输出读取的整数fmt.Printf("Read a value from intChan: %d\n", value)}
}

运行上面的代码会产生如下的 panic 信息:

panic: send on closed channelgoroutine 1 [running]:
main.main()/path/to/main.go:15 +0x10b
exit status 2

可以看到,向已关闭的 channel 中写入数据会导致 panic,程序崩溃。

在这段代码中,我们创建了一个容量为 10 的整数类型的 channel intChan。然后我们向 intChan channel 中写入了 5 个整数。接着,我们使用 close 函数关闭了 intChan channel。接着,我们向已关闭的 intChan channel 中写入了整数 6,这时会导致 panic。最后,我们使用一个循环从 intChan channel 中读取所有整数,并将它们输出。

channel的遍历

channel遍历的方式

在 Go 中,可以通过 for 循环遍历 channel 中的数据,有以下几种方式:

1.使用 range 关键字遍历 channel

// 创建一个容量为 3 的整数类型的 channel
intChan := make(chan int, 3)// 向 channel 中写入 3 个整数
intChan <- 1
intChan <- 2
intChan <- 3// 使用 range 关键字遍历 channel
for value := range intChan {fmt.Printf("Read a value from intChan: %d\n", value)
}

在这段代码中,我们创建了一个容量为 3 的整数类型的 channel intChan,并向其中写入了 3 个整数。接着,我们使用 range 关键字遍历了 intChan channel 中的所有数据,并将它们输出。当 channel 被关闭后,使用 range 关键字遍历 channel 会自动退出循环。

2.使用 for 循环和 select 语句遍历 channel

// 创建一个容量为 3 的整数类型的 channel
intChan := make(chan int, 3)// 向 channel 中写入 3 个整数
intChan <- 1
intChan <- 2
intChan <- 3// 使用 for 循环和 select 语句遍历 channel
for {select {case value, ok := <-intChan:// 如果 channel 已经关闭且里面没有数据了,就退出循环if !ok {return}// 输出读取的整数fmt.Printf("Read a value from intChan: %d\n", value)}
}

在这段代码中,我们创建了一个容量为 3 的整数类型的 channel intChan,并向其中写入了 3 个整数。接着,我们使用 for 循环和 select 语句遍历了 intChan channel 中的所有数据,并将它们输出。当 channel 被关闭后,使用 for 循环和 select 语句遍历 channel 需要手动退出循环。

3 使用 len 函数和 for 循环遍历 channel(难点)

1) i < len(intChan) 陷入死循环
// 创建一个容量为 3 的整数类型的 channel
intChan := make(chan int, 3)// 向 channel 中写入 3 个整数
intChan <- 1
intChan <- 2
intChan <- 3// 使用 len 函数和 for 循环遍历 channel
for i := 0; i < len(intChan); i++ {// 从 channel 中读取一个整数value := <-intChan// 输出读取的整数fmt.Printf("Read a value from intChan: %d\n", value)
}

在这段代码中,我们创建了一个容量为 3 的整数类型的 channel intChan,并向其中写入了 3 个整数。接着,我们使用 len 函数和 for

但是,这段代码会输出前两个整数,然后进入死循环,因为在for循环中使用了len(intChan)作为循环条件,但是在循环的过程中我们不断地从channel中读取数据,导致channel的长度始终保持为1,不会发生变化,从而导致for循环无法退出。

解决方案1:使用cap(intChan)函数获取channel的容量

正确的做法是在循环条件中使用cap(intChan)函数获取channel的容量,如下所示:

// 创建一个容量为 3 的整数类型的 channel
intChan := make(chan int, 3)// 向 channel 中写入 3 个整数
intChan <- 1
intChan <- 2
intChan <- 3// 使用 len 函数和 for 循环遍历 channel
for i := 0; i < cap(intChan); i++ {// 从 channel 中读取一个整数value := <-intChan// 输出读取的整数fmt.Printf("Read a value from intChan: %d\n", value)
}

这样就可以正确地遍历整个channel,输出所有的整数了。

2) 只等读到一半的数据
package mainimport "fmt"func main() {// 创建一个容量为 3 的整数类型的 channelch := make(chan int, 30)// 向 channel 中写入 30 个整数for i := 0; i < 30; i++ {ch <- i}close(ch)// 使用 len 函数和 for 循环遍历 channelfor i := 0; i < len(ch); i++ {// 从 channel 中读取一个整数value := <-ch// 输出读取的整数fmt.Printf("Read a value from intChan: %d\n", value)}//for len(ch) > 0 {//    fmt.Println(<-ch)//}
}

输出结果

Read a value from intChan: 0
Read a value from intChan: 1
Read a value from intChan: 2
Read a value from intChan: 3
Read a value from intChan: 4
Read a value from intChan: 5
Read a value from intChan: 6
Read a value from intChan: 7
Read a value from intChan: 8
Read a value from intChan: 9
Read a value from intChan: 10
Read a value from intChan: 11
Read a value from intChan: 12
Read a value from intChan: 13
Read a value from intChan: 14
解决方案2:for len(ch) > 0 {}

如何使用len()得到正确结果

for len(ch) > 0{}
package mainimport "fmt"func main() {// 创建一个容量为 3 的整数类型的 channelch := make(chan int, 30)// 向 channel 中写入 30 个整数for i := 0; i < 30; i++ {ch <- i}close(ch)// 使用 len 函数和 for 循环遍历 channel//for i := 0; i < len(ch); i++ {//  // 从 channel 中读取一个整数//  value := <-ch// // 输出读取的整数//    fmt.Printf("Read a value from intChan: %d\n", value)//}for len(ch) > 0 {value := <-ch// 输出读取的整数fmt.Printf("Read a value from intChan: %d\n", value)}
}

结果如下

Read a value from intChan: 0
Read a value from intChan: 1
Read a value from intChan: 2
Read a value from intChan: 3
Read a value from intChan: 4
Read a value from intChan: 5
Read a value from intChan: 6
Read a value from intChan: 7
Read a value from intChan: 8
Read a value from intChan: 9
Read a value from intChan: 10
Read a value from intChan: 11
Read a value from intChan: 12
Read a value from intChan: 13
Read a value from intChan: 14
Read a value from intChan: 15
Read a value from intChan: 16
Read a value from intChan: 17
Read a value from intChan: 18
Read a value from intChan: 19
Read a value from intChan: 20
Read a value from intChan: 21
Read a value from intChan: 22
Read a value from intChan: 23
Read a value from intChan: 24
Read a value from intChan: 25
Read a value from intChan: 26
Read a value from intChan: 27
Read a value from intChan: 28
Read a value from intChan: 29

思考:channel的遍历中for len(ch) > 0{ } 能得到全部结果,然而for i := 0; i < len(ch); i++{ }只能得到一半结果

在使用for i := 0; i < len(ch); i++遍历channel时,由于channel在遍历过程中长度是动态变化的,因此可能只能读取到channel的一部分数据。具体来说,当channel中的数据量大于循环开始时的长度时,只能读取到前一半的数据。同时,如前面例子所示,当len(ch)=3时,会陷入死循环。

相比之下,for len(ch) > 0 {} 的循环条件是根据 channel 的实际长度来判断的,因此只要 channel 中还有元素,循环就会继续执行,直到 channel 中的所有元素都被读取完毕。因此,这种方式可以保证读取到全部的结果。

遍历时,如果channel没有关闭,则会出现deadlock的错误

channel支持for–range的方式进行遍历,请注意两个细节

  1. 在遍历时,如果channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。如前文所述;
    2)在遍历时,如果channel没有关闭,则回出现deadlock的错误

一个会导致deadlock错误的例子:

package mainimport "fmt"func main() {ch := make(chan int)ch <- 1ch <- 2ch <- 3for i := range ch {fmt.Println(i)}
}

在这个例子中,我们创建了一个int类型的channel,并向该channel中写入了三个数据。然后我们使用for range循环遍历该channel,并输出其中的数据。但是由于我们没有关闭channel,当循环遍历完前三个数据后,程序会一直阻塞等待新的数据,导致deadlock错误。

fatal error: all goroutines are asleep - deadlock!

为了避免这种错误,我们可以使用close函数在遍历完所有数据后手动关闭channel,例如:

package mainimport "fmt"func main() {ch := make(chan int)ch <- 1ch <- 2ch <- 3close(ch)for i := range ch {fmt.Println(i)}
}

在这个例子中,我们在循环遍历之前,使用close函数手动关闭了channel。这样,在遍历完所有数据后,for range循环会自动退出,避免了deadlock错误的发生。

3 实例演示:定义map型channel,并进行数据的读写

map型channel

package mainimport "fmt"func main() {ch := make(chan map[string]string, 10) // 创建容量为 10 的 channel// 向 channel 中输入 6 个数据for i := 1; i <= 6; i++ {data := make(map[string]string)data["key"] = fmt.Sprintf("value %d", i)ch <- data}// 从 channel 中取出并输出 6 个数据for i := 1; i <= 6; i++ {data := <-chfmt.Println(data["key"])}
}
value 1
value 2
value 3
value 4
value 5
value 6

map型channel2:

package mainimport "fmt"func main() {mapchan := make(chan map[string]string, 10) // 创建容量为 10 的 channel// 创建两个 map 变量 m1 和 m2,并向其中各写入 5 个数据m1 := make(map[string]string, 20)m1["key1"] = "value1"m1["key2"] = "value2"m1["key3"] = "value3"m1["key4"] = "value4"m1["key5"] = "value5"m2 := make(map[string]string, 20)m2["key6"] = "value6"m2["key7"] = "value7"m2["key8"] = "value8"m2["key9"] = "value9"m2["key10"] = "value10"// 将 m1 和 m2 中的数据写入到 mapchan channel 中mapchan <- m1mapchan <- m2// 从 mapchan channel 中取出并输出所有数据for len(mapchan) > 0 {m := <-mapchanfor k, v := range m {fmt.Printf("%s: %s\n", k, v)}}
}

输出结果

key1: value1
key2: value2
key3: value3
key4: value4
key5: value5
key6: value6
key7: value7
key8: value8
key9: value9
key10: value10

在这段代码中,我们创建了一个容量为 10 的 mapchan channel,并创建了两个 map 变量 m1 和 m2,每个变量中各包含 5 个键值对。接下来,将 m1 和 m2 中的数据分别写入到 mapchan channel 中。然后使用一个循环从 mapchan channel 中读取数据,直到 channel 中没有剩余的数据。在循环体中,我们取出 channel 中的一个 map 变量 m,并遍历其中的键值对并输出它们。需要注意的是,在循环中我们使用 len(mapchan) > 0 来判断 channel 中是否还有剩余的数据,因为 len(mapchan) 表示 channel 中剩余的数据数量。由于我们在循环体中读取了所有数据,因此在循环结束后,mapchan channel 中不再有任何数据。

Struct型channel

package mainimport "fmt"type Person struct {Name stringAge  int
}func main() {// 创建一个容量为 3 的 Person 类型的 channelpersonChan := make(chan Person, 3)// 向 channel 中写入 3 个 Person 类型的变量person1 := Person{"Tom", 18}person2 := Person{"Jerry", 19}person3 := Person{"Mike", 20}personChan <- person1personChan <- person2personChan <- person3// 从 channel 中取出并输出所有 Person 类型的变量for len(personChan) > 0 {person := <-personChanfmt.Printf("Name: %s, Age: %d\n", person.Name, person.Age)}
}

通过空接口实现存放任意数据类型的channel

可以存放任意数据类型的channel:通过空接口实现
在下面这个例子中,我们将多个不同类型的数据写入了 dataChan channel,然后按照它们写入的顺序一个一个地取出来。输出结果显示了每个元素的值,其中包括整数、字符串、布尔值、浮点数、整数数组、空的映射、包含一个整数字段的结构体、空指针、字节数组以及包含字符串、整数和布尔值的接口切片。由于我们使用了 interface{} 类型,可以存储任何类型的数据。

package mainimport "fmt"func main() {// 创建一个容量为 10 的 interface{} 类型的 channeldataChan := make(chan interface{}, 10)// 向 channel 中写入 10 个任意类型的数据dataChan <- 1dataChan <- "hello"dataChan <- truedataChan <- 3.14dataChan <- []int{1, 2, 3}dataChan <- make(map[string]int)dataChan <- struct{ X int }{X: 10}dataChan <- nildataChan <- []byte("world")dataChan <- []interface{}{"a", 1, false}// 从 channel 中取出并输出所有数据for len(dataChan) > 0 {data := <-dataChanfmt.Printf("%v\n", data)}
}
1
hello
true
3.14
[1 2 3]
map[]
{10}
<nil>
[119 111 114 108 100]
[a 1 false]

空接口型channel与类型断言

在 Go 中,我们可以使用空接口类型 interface{} 表示任意类型的值。当我们将一个具体类型的值存储到空接口类型的变量中时,这个变量会自动地被转换为 interface{} 类型。但是,在从空接口类型的变量中取出具体类型的值时,我们需要使用类型断言来将其转换回原来的类型。

下面是一个在空接口类型的 channel 中使用类型断言的例子:

package mainimport "fmt"func main() {// 创建一个容量为 10 的空接口类型的 channeldataChan := make(chan interface{}, 10)// 将多个不同类型的数据写入 channel 中dataChan <- 1dataChan <- "hello"dataChan <- []int{1, 2, 3}dataChan <- struct{ X int }{X: 10}// 从 channel 中取出数据并进行类型断言for len(dataChan) > 0 {data := <-dataChanswitch v := data.(type) {case int:fmt.Printf("Got an int: %d\n", v)case string:fmt.Printf("Got a string: %s\n", v)case []int:fmt.Printf("Got an int slice: %v\n", v)case struct{ X int }:fmt.Printf("Got a struct: %+v\n", v)default:fmt.Printf("Got something else: %v\n", v)}}
}

在这段代码中,我们创建了一个容量为 10 的空接口类型的 channel dataChan。然后我们向 dataChan channel 中写入了 4 个不同类型的数据,包括 int、string、[]int 和 struct{X int}。最后,我们使用一个循环从 dataChan channel 中取出所有数据,并根据其类型进行类型断言。如果数据是 int 类型,我们就输出 "Got an int: " 和该值;如果数据是 string 类型,我们就输出 "Got a string: " 和该值;以此类推。

输出结果如下:

Got an int: 1
Got a string: hello
Got an int slice: [1 2 3]
Got a struct: {X:10}

在这个例子中,我们使用了类型断言来将从 dataChan channel 中取出的值转换回原来的类型。由于我们在写入数据时将它们转换成了空接口类型,因此在取出数据时需要使用类型断言将它们转换回原来的类型。

Channel的定义、写入、读取、关闭与遍历相关推荐

  1. JAVA基础初探(十二)Map接口及其常用实现类(HashMap)、File类详解(概述、创建、删除、重命名、文件属性读取/设置、遍历文件夹)

    该篇博客目录 1.Map接口及其常用实现类(HashMap.Hashtable) 2.File类(概述.创建.删除.重命名.文件属性读取/设置.遍历文件夹) 一.Map接口及其常用实现类(HashMa ...

  2. Flume 1.7 源码分析(五)从Channel获取数据写入Sink

    Flume 1.7 源码分析(一)源码编译 Flume 1.7 源码分析(二)整体架构 Flume 1.7 源码分析(三)程序入口 Flume 1.7 源码分析(四)从Source写数据到Channe ...

  3. android逐行写入读取_Android外部存储-读取,写入,保存文件

    android逐行写入读取 Android external storage can be used to write and save data, read configuration files ...

  4. STM32F103C8T6通过内部Flash写入读取数据,模拟EEPROM(附代码)

    STM32F103C8T6通过内部Flash写入读取数据,模拟EEPROM(附代码) 优点: 1. 模块化编程,方便移植,集成度高: 2. 拿来直接用 Flash空间定定义 主函数初始化已经Flash ...

  5. python xlrd读取excel所有数据_python读取excel进行遍历/xlrd模块操作

    我就废话不多说了,大家还是直接看代码吧~ #!/usr/bin/env python # -*- coding: utf-8 -*- import csv import xlrd import xlw ...

  6. ShardingSphere(四) 垂直分库配置搭建,实现写入读取

    概述:垂直分库即转库专用,不同的数据库中存放不同的表信息.比如学生和课程信息,我们将课程表存放与一个数据库中,学生信息存储于另一个库中,本章将介绍如何通过配置Sharding jdbc实现垂直分库操作 ...

  7. ShardingSphere(三) 水平分库配置搭建,实现写入读取

    概述:本章将介绍如何通过Sharding jdbc来实现数据库的水平分库操作,并从零搭建一个SpringBoot工程实现分库的读写操作.本文章在上一章节基础上改造,其中内容包含了表的水平拆分内容 环境 ...

  8. 错误:Attempted read from closed stream尝试读取关闭的流!!!

    错误:Attempted read from closed stream尝试读取关闭的流!!! 原因:一个HttpClient里只能有一次获取Entity的操作,不能有多次获取Entity的操作,ge ...

  9. C# Winfor界面写入读取Excel

    C# Winfor界面写入读取Excel 一.创建项目 二.制作UI界面,所需控件为textbox三个.Button四个.四个Label控件 三. 安装库,进入NuGet管理解决方案搜索Close X ...

最新文章

  1. 工艺路线和工序有差别吗_ERP-工序与工艺路线
  2. linux新用户登陆密码,如何强制Linux用户在第一次登录时更改初始密码?
  3. 【转】NHIBERNATE的各种保存方式的区别 (SAVE,PERSIST,UPDATE,SAVEORUPDTE,MERGE,FLUSH,LOCK)
  4. 开发实践 | 使用Android开发TCP、UDP客户端(代码类)
  5. php serialize取值,PHP 序列化(serialize)格式详解
  6. tcp http socket
  7. 动态查找表之二叉搜索树
  8. 渲染上下文Rendering Context
  9. 【C++】C++中的迭代器
  10. ubuntu(乌班图) 修改ip
  11. ios 基础知识点总结
  12. 小马哥----山寨高仿小米5 图片1:1机型 机型曝光 与真假鉴别方法
  13. 读书笔记之《阿里传:这是阿里巴巴的世界》
  14. 推荐几个图标搜索网站
  15. 【DC系列】DC-4靶机渗透练习
  16. jquery给input赋值 val()方法
  17. SketchUp: Modeling from Photos SketchUp教程:从照片建模 Lynda课程中文字幕
  18. 视图属性-详细参数解释
  19. Android 银联控件支付开发流程
  20. Uber火了!它改变了哪些营销游戏规则?

热门文章

  1. 让windows 2008 也netmeeting
  2. os error os5_汽车OS竞赛
  3. 解决jeb提示秘钥注册码过期
  4. 自媒体运营之:微博认证怎么弄黄v?(个人橙v认证)
  5. 第三天:ptyhon基础知识
  6. jzoj 6798. 【2014广州市选day2】regions
  7. 雷达信号处理基础 距离方程
  8. 学习笔记 | 深度森林 Deep Forest
  9. JVM性能调优篇07-阿里巴巴Arthas工具详解
  10. 【python辅助excel】(3)