Go数据类型
Go语言中只有强制类型转换,没有隐式类型转换。T(表达式)
Go字符串常用函数
-
统计字符串的长度,按字节。len(str)
-
字符串遍历,同时处理有中文的问题。 r := []rune(str)
-
字符串转整数:str,_=strconv.Atoi(“12”)
-
整数转字符串:str = strconv.Itoa(1234)
-
字符串转[]byte:var bytes = []byte(“hello go”)
-
[]byte转字符串:str = string([]byte(97,98,99))
-
10进制转2,6,8,16进制:str=strconv.FormatInt(123,2)!
-
查找字符串是否在指定的字符串中:strings.Contains(“seafood”,"foo)
-
统计一个字符串有几个指定的字串:strings.Count(“ceheebs”,"e)
-
不区分大小写的字符串比较(==是区分字母大小写的):fmt.Println(strings.EqualFold(“abc”,"abc))
-
返回子串在字符串出现的index值,如果没有返回-1:strings.Index(“LDE_abc”,“abc”)
-
返回子串在字符串最后一次出现的index,如果没有返回-1:strings.LastIndex(“go lang”,“go”)
-
将指定的子串替换成另外一个子串:strings.Replace(“go go hello”,“go”,“go语言”,n)n可以指定你希望替换几个,如果n=-1表示全部替换。
-
按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组,strings.Split(“hello,world”,“,”)
-
将字符串的字母进行大小写的替换:strings.ToLower(“Go”)
-
将字符串左右两边的空格去掉:strings.TrimSpace(“tn a long gopher ntrm”)
-
将字符串左右两边指定的字符去掉:strings.Trim(“!hello!”,“!”)
-
将字符串左边指定的字符去掉:strings.TrimLeft(“!hello!”,“!”)
-
将字符串右边边指定的字符去掉:strings.TrimRight(“!hello!”,“!”)
-
判断字符串是否以指定的字符串开头:strings.HasPrefix(“ftp://192.168.40.129”,“ftp”)
-
判断字符串是否以指定的字符串结束:strings.HasSuffix(“NLT_abc.jpg”,“abc”)
数组
数组可以存放多个统一数据类型的数据。
数组初始化
var arr01 [3]int = [3]int{1, 2, 3}
fmt.Println(arr01)
var arr02 = [...]int{2, 3, 4}
fmt.Println(arr02)
var arr03 = [...]int{1 : 2, 0 : 1, 2 : 3}
fmt.Println(arr03)
// 类型推导
arr04 := [4]int{2, 3, 4}
fmt.Println(arr04)
控制台输出为
[1 2 3]
[2 3 4]
[1 2 3]
[2 3 4 0]
数组遍历
for i := 0; i < len(arr04); i++ {
fmt.Println(arr04[i])
}
for i, v := range arr04 {
fmt.Printf("数组索引为%d, 值为%d",i,v)
fmt.Println()
}
控制台输出为
2
3
4
0
数组索引为0, 值为2。
数组索引为1, 值为3。
数组索引为2, 值为4。
数组索引为3, 值为0。
切片
底层
切片是一个引用类型。
切片定义后还不能使用,因为本身是一个空的。需要引用到一个数组或者make一个空间供切片使用。
切片底层来说就是一个结构体,元素个数 len()、切片容量cap()
type slice strut{
ptr *[2]int
len int
cap int
}
声明方式、扩容机制
-
定义一个切片,然后让切片去引用一个已经创建好的数组
// 定义一个数组,然后让切片去引用一个已经创建好的数组 a := [4]int{1, 2, 3, 4} s := a[1:3] // s := a[low:high] fmt.Printf("数组a第二个元素的地址值为:%p \n",&a[1]) fmt.Printf("切片s的一个元素的的地址为: %p \n", &s[0]) s = append(s,1) s = append(s, 3) s = append(s,4) fmt.Printf("切片s第三个元素地址值为:%p \n",&s[2]) fmt.Printf("数组a为:%v\n",a) fmt.Printf("s:%v len(s):%v cap(s):%v\n", &s, len(s), cap(s)) 控制台输出为: 数组a第二个元素的地址值为:0xc0000121a8 切片s的一个元素的的地址为: 0xc0000121a8 切片s第三个元素地址值为:0xc00000c430 数组a为:[1 2 3 1] s:&[2 3 1 3 4] len(s):5 cap(s):6
- 通过make创建切片,语法 var 切片名 []type = make([]type,len,cap{可选})
s1 := make([]int,2)
// s1 = append(s1, 1)
s1[0] = 1
s1 = append(s1,3)
fmt.Println(s1)
控制台输出为:
[1 0 3]
Map
介绍
基本语法:var 变量名 map[keytype]valuetype。声明不会分配内存,需要make
key 类型
golang中的map的key可以是很多种类型,比如bool,数字,string,指针,管道,还可以是包含前面几个类型的接口、结构体、数组。通常为int,string。 注意:slice、map还有fuction不可以,以为这几个没法用 == 来判断。
value类型
基本和key一样。通常为 数字、string、map、struct
总结:切片的长度不能省略,map的长度在make时可以省略。
声明方式
// 第一种声明方式
var map1 map[string]int
map1 = make(map[string]int,3)
map1["0"] = 0
fmt.Println(map1)
// 第二种声明方式
map2 := make(map[string]int)
map2["1"] = 1
fmt.Println(map2)
// 第三种声明方式
map3 := map[string]int{
"2" : 2, // 注意逗号不要遗漏
}
fmt.Println(map3)
控制台输出为:
map[0:0]
map[1:1]
map[2:2]
CRUD
删除
map4 := map[int]string{
0 : "小明",
1 : "小红",
2 : "小王",
}
delete(map4,0)
fmt.Println(map4)
控制台输出:
map[1:小红 2:小王]
查找
val1 := map4[3]
fmt.Println(val1)
val := map4[2]
fmt.Println(val)
控制台输出为:
小王
添加
map4[4] = "赵四"
fmt.Println(map4)
控制台输出为:
map[1:小红 2:小王 4:赵四]
遍历
for range方式
数量
len()
Map排序
高版本的go进行 fmt 输出是默认按照key排序的,低版本的是无序的。但是不论高版本还是低版本进行for range进行遍历输出时,都是无序输出的。要想遍历按照顺序输出,则需要把所有key放到切片中 ,在切片中对key进行排序。
map1 := make(map[string]string)
map1["2"] = "a"
map1["1"] = "b"
map1["3"] = "c"
for key, val := range map1 {
fmt.Printf("索引为: %s, 值为:%s \n", key, val)
}
fmt.Println(map1) // 高版本 按照key排好了顺序
keyList := make([]string,0)
for key, _ := range map1 {
keyList = append(keyList, key)
}
// 对列表进行排序
sort.Strings(keyList)
// 遍历key的链表 并输出
for _, key := range keyList {
fmt.Printf("索引为: %s , 值为: %s\n", key,map1[key])
}
Map切片
就是切片里的数据类型是map,切片要make,切片对应的数据类型是map也要make
声明: var a = make([]map[string]string) var a = []map[string]string 这两种是错误的,因为切片需要在make时说明数量
正确做法:var a = make([]map[string]string, 1) 或者 a := make([]map[string]string, 1)
a := make([]map[string]string,3)
map5 := map[string]string{
"1" : "1",
}
a = append(a,map5)
fmt.Println(a)
控制台输出为:
[map[] map[] map[] map[1:1]]
若要修改切片里的map值
a[0] = map[string]string{
"2" : "2",
}
fmt.Println(a)
控制台输出为:
[map[2:2] map[] map[] map[1:1]]
结构体
声明
type 结构体名称 struct{
field1 type
field2 type
}
// 声明一个结构体
type Person struct {
Id int
Name string
money float32
ptr *int
slice []int
map1 map[string]string
}
func main() {
// 实例化一个结构体
var p1 = Person{
Id: 0,
Name: "",
money: 0,
ptr: nil,
slice: nil,
map1: nil,
}
p1.slice = make([]int,3)
p1.slice[0] = 1
p1.map1 = make(map[string]string)
p1.map1["k1"] = "val1"
fmt.Println(p1)
}
控制台输出为:
{0 0 <nil> [1 0 0] map[k1:val1]}
注意事项和细节说明:
-
字段声明语法同变量,示例:字段名 字段类型
-
字段的类型可以为:基本类型、数组或引用类型
-
在创建一个一个结构体变量后,如果没有给字段赋值,都对应一个默认值:布尔类型是false,数值类型为0,字符串为"",数组的默认值和他的元素类型相关,指针、slice、和map的默认值都为nil。
创建一个结构体实例
方式一:直接声明
var person Person;
方式二:
var person Person = Person{} 或者person := Person{“111”,22}
方式三:
var person *Person = new(Person)
结构体指针访问字段的标准方式应该是:(*结构体).字段名 但是可以简化为 结构体指针.字段名。
方式四:
var person *Person = &Person{}
var p2 = new(Person)
/**
相当于
var p3 = Person{}
p2 := &p3
*/
p2.Id = 2
fmt.Printf("p2对象的类型为:%T\n" , p2)
fmt.Printf("指针指向的地址为:%p \n", p2)
fmt.Printf("指针本身的地址为:%v \n", &p2)
控制台输出为:
p2对象的类型为:*main.Person
指针指向的地址为:0xc0001000f0
指针本身的地址为:0xc00014c020
结构体注意事项
- 结构体中所有字段在内存中是连续的。
- 结构体的字段完全一样(个数、字段名、类型)才可以转换。
- 结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转。
工厂模式
package model
type stu struct {
Name string
Age int
}
func NewStudent(name string, age int) *stu {
return &stu{
Name: name,
Age: age,
}
}
package main
import (
model "algorithm/Demo/struct"
"fmt"
)
func main() {
stu := model.NewStudent("小花",3)
fmt.Println(stu)
}
方法
介绍
方法是作用在指定数据类型上的(即:和指定的数据类型绑定),因此自定义数据类型,都可以有方法,而不仅仅是struct。
type A struct{
Num int
}
func (a A)test(){
fmt.Println(a.NUm)
}
方法的调用和传参和getter、setter方法
调用机制和函数一样。不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法**(如果变量是值变量类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)**
package main
import "fmt"
type stu struct {
Id int
name string // 注意
Math int
English int
Chinese int
}
func (this stu) scoreAll() int {
return this.Math + this.English + this.Chinese
}
func (this *stu) getName() string {
return this.name
}
func main() {
stu1 := stu{
Id: 0,
name: "小明",
Math: 90,
English: 80,
Chinese: 70,
}
fmt.Printf("stu1的总成绩为:%d \n",stu1.scoreAll())
stu2 := stu{
Id: 1,
name: "小红",
Math: 80,
English: 80,
Chinese: 98,
}
fmt.Printf("stu2的地址值为:%p \n",&stu2)
fmt.Printf("stu2.name的地址值为:%p \n",&stu2.name)
fmt.Printf("stu2的名字为: %s \n",stu2.getName())
}
控制台输出为
stu1的总成绩为:240
stu2的地址值为:0xc000078480
stu2.name的地址值为:0xc000078488
stu2的名字为: 小红
细节
方法的注意事项和细节讨论
- 结构体类型是值类型,在方法调用中,遵守值类型传递机制,是值拷贝传递方式。
- 如果想要修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang中的方法作用在指定的数据类型上。因此自定义类型都可以有方法。
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,则也一再本包和其他包访问。
- 如果一个变量实现了String()这个方法,那么fmt.Println()默认会调用这个变量的String()进行输出。
封装
实现步骤:
- 将结构体、字段(属性)的首字母小写(不能导出),使其他包不能使用,类似private。
- 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数。
- 提供一个首字母大写的Set方法(类似其他语言的public),用于对属性判断并赋值
func (var 结构体类型名) SetXxx(参数列表)(返回值列表){
// 加入数据验证的业务逻辑
var.字段 = 参数
}
- 提供一个首字母大写的Get方法(类似于其它语言的public),用于获取属性的值。
func (var 结构体类型) GetXxx(){
return var.字段;
}
继承
在Golang中如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现继承特性。
type Goods struct {
Name string
Price int
}
type Book struct {
Goods // 这里就是嵌套了匿名结构体Goods
Writer string
}
type Goods struct {
Name string
price int
}
func (g *Goods) getName() string {
return g.Name
}
type Book struct {
Goods // 这里就是嵌套了匿名结构体
Writer string
}
//func (b Book) getName() string {
// return "淦"
//
//}
func main() {
book := &Book{}
book.Name = "十万个为什么"
book.Goods.price = 10
book.Writer = "author"
fmt.Println(book)
name := book.getName()
fmt.Printf("getName()返回的名字为:%s \n", name)
}
控制台输出为:
&{{十万个为什么 10} author}
getName()返回的名字为:你好
- 结构体可以使用嵌套匿名结构体的所有方法的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。
- 匿名结构体字段可以简化。
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体来区分。
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定你匿名结构体名字,否则会编译报错。
- 如果一个struct嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段和方法时,必须带上结构体名字。
- 嵌套匿名结构体后,也可以在创建结构体实例时,直接指定各匿名结构体的名字。
接口
type Usb interface {
// 声明两个方法
Start()
Stop()
}
type Phone struct {
}
// 让手机实现Usb接口
func (p Phone) Start() {
fmt.Println("手机开始工作")
}
func (p Phone) Stop() {
fmt.Println("手机停止工作")
}
type Computer struct{
}
// 编写一个Working方法,接受一个Usb接口类型的变量
// 只要是实现了 Usb接口(所谓实现Usb接口,就是指实现了 Usb接口声明的所有方法)
func (c Computer) Working(usb Usb) {
// 通过usb接口变量来调用start和stop方法
usb.Start()
usb.Stop()
}
func main() {
computer := Computer{}
phone := Phone{}
computer.Working(phone)
}
控制台输出为:
手机开始工作
手机停止工作
基本介绍
interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。
基本语法
type 接口名 interface{
method1(参数列表) 返回值列表
method2(参数列表) 返回值列表
}
实现接口方法
func (t 自定义类型) method1(参数列表) 返回值列表{
// 方法实现
}
func (t 自定义类型) method2(参数列表) 返回值列表{
// 方法实现
}
说明:
- 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低耦合的思想。
- Golang中的接口,不需要显示的实现。只需要一个变量,含有接口类型中的所有方法,那么这个变量就实现了这个接口。
注意事项和细节
-
接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的实例。
// 定义接口 type AInterface interface { Say() } // 定义结构体 type B struct { Name string } // 实现结构体方法 隐式实现了接口 func (b B) Say() { fmt.Println("Person Say()") } func main() { var b B var a AInterface = b a.Say() } 控制台输出为: Person Say()
-
接口中所有方法都没有方法体。
-
在Golang中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
-
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。
-
只要有自定义数据类型,就可以实现接口,不仅仅是结构体类型。
type AInterface interface { Say() } // 定义非结构体type type integer int func (i integer) Say() { fmt.Println("integer Say i= ",i) } func main(){ var i integer = 10 var b AIntegerface = i b.say() // integer Say i= 10 }
-
一个自定义类型可以实现多个接口
-
golang接口中不能有任何变量
-
一个接口可以继承多个别的接口(b,c接口),这是如果要实现a接口,也必须将b,c接口的方法全部实现。
type Ai interface { test01() test02() } type Bi interface { test01() test03() } type Ci interface { Ai Bi } type P struct { name string } func (p *P) test01() { fmt.Println("A-----test01()") } func (p *P) test02() { fmt.Println("A-----test02()") } func (p *P) test03() { fmt.Println("B-----test03()") } func main() { var person *P var a Ai = person a.test01() var b Bi = person b.test01() b.test03() var c Ci = person c.test02() } 控制台输出为: A-----test01() A-----test01() B-----test03() A-----test02()
-
interface类型是一个指针(引用类型),如果没有对interface初始化就是用,那么就会输出nil
-
空接口没有任何方法,所以所有类型都实现了空接口,即我们可以把任何一个变量赋给空接口。
func main() { // 定义一个空接口x var x interface{} s := "Hello 沙河" x = s fmt.Printf("type:%T value:%v\n", x, x) i := 100 x = i fmt.Printf("type:%T value:%v\n", x, x) b := true x = b fmt.Printf("type:%T value:%v\n", x, x) } 控制台输出: type:string value:Hello 沙河 type:int value:100 type:bool value:true
检查结构体是否实现了接口
type methoder interface {
A()
B()
}
type Struct1 struct {
name string
age int
}
func (s Struct1) A() {
fmt.Println("struct1 --- A")
}
func (s Struct1) B() {
fmt.Println("struct1 --- A")
}
func main() {
fmt.Println("--------方法一------------")
// 方法一
var a methoder = new(Struct1)
var b methoder = &Struct1{}
fmt.Printf("%v\n", a)
fmt.Printf("%v\n", b)
fmt.Println("--------方法二------------")
// 方法二
var c methoder = (*Struct1)(nil)
fmt.Printf("%v\n", c)
fmt.Println("--------方法三------------")
//方法三 类型断言判别
var struct1 Struct1
var niler methoder
niler = struct1
_, ok := niler.(Struct1)
fmt.Println(ok)
}
类型断言
引出:如何将一个接口变量,赋给自定义类型的变量。
注意:只能转化成原来的类型。
type Point struct {
x int
y int
}
func main() {
var a interface{}
var point Point = Point{1,2}
a = point
var b Point
//b = a //error
b = a.(Point)
fmt.Println(b)
}
类型判断
func TypeJudge(item... interface{}) {
for index, val := range item {
switch val.(type) {
case int:
fmt.Printf("第 %d 个参数是int类型,值为 %v \n", index, val)
case bool:
fmt.Printf("第 %d 个参数是bool类型,值为 %v \n", index, val)
default:
fmt.Printf("不确定类型是什么")
}
}
}
协程(goroutine)
相关概念
进程和线程说明
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的单位。
- 线程是进程的一个执行实例,是程序执行的最小单位,他是比进程更小的能独立运行的基本单位。
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发的执行。
- 一个程序至少有一个进程,一个进程至少有一个线程。
并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
GOMAXPROCS
runtime.GOMAXPROCS(逻辑CPU数量)
这里的逻辑CPU数量可以有如下几种数值:
- <1:不修改任何数值。
- =1:单核心执行。
- >1:多核并发执行。
func main() {
fmt.Printf("CPU核心数:%d\n", runtime.NumCPU())
runtime.GOMAXPROCS(2)
go task()
go task()
go task()
go task()
select{}
}
func task(){
str := ""
for {
str = str + "1"
}
}
协程特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
启动goroutine
启动单个goroutine
启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go
关键字。
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
// runtime.Gosched()
}
这一次的执行结果只打印了 main goroutine done!,并没有打印Hello Goroutine!
。这是因为主线程结束时协程将会关闭。
解决这种的方式最简单粗暴的就是time.Sleep
了。(还没等到子协程执行,主协程就已经执行完退出了,子协程将不再执行)
还有一种解决方法 runtime.Gosched()
让当前goroutine让出CPU,好让其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行。
func hello(i int) {
fmt.Println("Hello Goroutine!", i)
}
func main() {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
go hello(i) // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!", i)
runtime.Gosched()
}
}
启动多个goroutine
启动多个goroutine
。 (这里使用了sync.WaitGroup
来实现goroutine的同步)
WaitGroup
对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait()
用来控制计数器的数量。Add(n)
把计数器设置为n
,Done()
每次把计数器-1
,wait()
会阻塞代码的运行,直到计数器地值减为0
。
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
控制台输出为:
Hello Goroutine! 0
Hello Goroutine! 7
Hello Goroutine! 8
Hello Goroutine! 3
Hello Goroutine! 9
Hello Goroutine! 1
Hello Goroutine! 5
Hello Goroutine! 6
Hello Goroutine! 2
Hello Goroutine! 4
channel
channel
是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
- channel本质是一个队列
- 数据先进先出
- channel是线程安全的,多个协程操作同一个管道时,不会发生资源竞争问题
- channel是有类型的,但是管道可以存放空接口类型,但是取数据需要强制转换(类型断言)。
操作
发送
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
接收
从一个通道中接收值。
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
关闭
我们通过调用内置的close
函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
无缓冲的通道
无缓冲的通道又称为阻塞的通道。无缓冲信道不存储值,无论是传值还是取值都会阻塞。
下面的代码会出现死锁,因为我们使用ch := make(chan int)
创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
解决方法:启动一个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("发送成功")
}
有缓冲的通道
使用有缓冲区的通道。可以在使用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)
}
chan<- int
是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;<-chan int
是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
select多路复用
在某些场景下我们需要同时从多个通道接收数据。下面代码影响性能。
for{
// 尝试从ch1接收值
data, ok := <-ch1
// 尝试从ch2接收值
data, ok := <-ch2
…
}
换成:
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
代码规范
注意:推荐 Uber Go 语言编码规范
https://github.com/xxjwxc/uber_go_guide_cn
1 文件
文件名应一律使用小写, 不同单词之间用下划线分割, 不用驼峰式,命名应尽可能地见名知意。
2 注释
// 注释
注意:// 与注释内容间有一个空格
3 命名规范
- 需要注释来补充的命名就不算是好命名。
- 使用可搜索的名称:单字母名称和数字常量很难从一大堆文字中搜索出来。单字母名称仅适用于短方法中的本地变量,名称长短应与其作用域相对应。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。
- 做有意义的区分:product 和 productInfo 和 productData 没有区别,nameString 和 name 没有区别,要区分名称,就要以读者能鉴别不同之处的方式来区分 。
- 函数命名规则:函数命名采用驼峰命名,其中单元测试中的测试函数,以及模块中需要导出的函数必须采用大驼峰命名,其他普通函数可以采用小驼峰命名。名字可以长但是得把功能,必要的参数描述清楚**,函数名应当是动词或动词短语**,如 postPayment、deletePage、save。
- 结构体命名规则:结构体名应该是名词或名词短语,如 Costume、WikiPage、Account、AddressParser,避免使用 Manager、Processor、Data、Info、这样的类名,类名不应当是动词。
- 包名命名规则:包名应该为小写单词,不要使用下划线或者混合大小写。
- 接口命名规则:单个函数的接口名以”er”作为后缀,如 Reader,Writer。接口的实现则去掉“er”。
4 测试文件
- 文件名必须是_test.go结尾的(文件名必须是*_test.go的类型,*代表要测试的文件名),这样在执行go test的时候才会执行到相应的代码
- 必须import testing这个包
- 所有的测试用例函数必须是Test开头(函数名必须以Test开头如:TestXxx或Test_xxx)
练习
适合有一定go基础或者看完本篇文章
Learn Go with tests
https://studygolang.gitbook.io/learn-go-with-tests/go-ji-chu/install-go