List [CTL]
在用Python写程序的过程中,由于GIL的存在,CPU密集型的并发变得效率低下,且为了构建一个设计精良的并发程序需要考虑很多问题
Go语言在底层设计好了并发模型,所以它生下来就是用来写大型并发应用的,最近学习了Go语言。由于它的语法是C-like的,加上基本不存在依赖问题,有问题随时查官方文档,所以学习起来进度还是挺快的。学习结束,总结一些Tips:
2018/12/4–update: 写多了发现,Golang在整个语言设计上很平庸,以及某些语法逻辑很混乱,无泛型(只能用interface{}
+ 类型断言替代,据说2.0要加上了),包管理很蛋疼(每次都import "github.com/xxx/xx"
,没有作为包的层次组织。go mod解决了这个痛点),错误处理…我就懒得说了,还有某些标准库的实现实在是过于原始
程序架构
在Go中,程序是以包为单位组织的,每个文件夹为一个包,每个程序的开头会有
package
语句声明自己属于哪个包。会编译成二进制文件的程序会声明自己为package main
包级公开变量以首字母大写作为标识,而私有变量以小写字母开头。不同于一般语言的下划线声明私有变量
init
函数用来对程序进行初始化,它在main
函数之前执行接口的方法集:
类型
*S
的可调用方法集包含接受者为*S
或S
的所有方法集类型
S
的可调用方法集只包含接受者为S
的方法集,不包含接受者为*S
的方法集
内嵌对象的方法集提升:
如果
S
包含一个匿名字段K
,S
和*S
的方法集都包含接受者为K
的方法提升如果
S
包含一个匿名字段*T
,S
和*S
的方法集都包含接受者为T
或者*T
的方法提升
Go语言函数返回局部变量指针,局部变量内存不会随函数栈帧弹出而销毁,不同于C/Cpp会造成野指针。原因是Go编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析escape analysis,当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。所以不用担心会不会导致memory leak,因为GO语言有强大的垃圾回收机制
//go
package main
import (
"fmt"
)
func foo() *int {
var i int64 = 199000000000
return &i
}
func main() {
pointer := foo()
fmt.Println(*pointer)
}
- 异常处理:
defer func() {
if r := recover(); r != nil {
log.Printf("Runtime error caught: %v", r)
}
}()
语法
Go中的指针与值的使用非常不严格,大多时候不需要明确进行解引操作,编译器会自动生成正确的语句。重要的是引用类型和值类型(严格来讲Go全部是值类型,引用类型只是底层值类型指针的封装),引用类型:hashmap, channel, interface, slice, function
对于可迭代对象,
range
返回的是元素的副本对于花括号包裹的数据类型的定义,单行时只需在每个元素间用逗号分割,多行时强制每个元素后加拖尾逗号
a := map[int]int{1: 1, 2: 2} a := map[int]int{ 1: 1, 2: 2, }
Go中的闭包内层函数一定是匿名函数,JavaScript也是这样:
package main import ( "fmt" ) func test() { test_v := 10 func() { test_v = 20 }() fmt.Println(test_v) } func main() { test() }
Go语言函数声明处指定函数返回值后,函数体内部无需声明该返回值
Go语言中语法很多与C/Cpp相反如,据说这是Thompson总结C语言经验得出的更优设计:
变量声明:
var a int
指针声明:
var a *int
,指针解引时相同:*a = 1
别名:
type myint int
,C/Cpp是本名在前,别名在后
make
函数用于切片,哈希表,通道的初始化,并返回对象;new
函数也是初始化上述三个类型,但返回指针,相当于C/Cpp的malloc
/new
数据类型/结构
Go的数组完全基于值语义,C/Cpp数组传递时基于引用语义,结构体中定义时基于引用语义
Go的切片行为:
import ( "fmt" ) type stu struct { tslice []int sk int } func (this stu) testfunc() { //this.tslice = append(this.tslice, 10) this.tslice[0] = 10 } func main() { a := stu{[]int{1, 2, 3, 4}, 1} a.testfunc() fmt.Println(a) } //append函数对切片的修改外部不可见
原因:
切片的内部实现:
struct Slice { byte* array; uintgo len; uintgo cap; };
函数内调用append修改了
len/cap
,但是结构体类型中的值为拷贝,故仅局部可见。不同于slice[0] = 1
的赋值是使用引用类型结构体中指针变量赋值Go的原生字符串用飘号包裹,
Json/Xml
标签也用飘号包裹数组与切片的声明区别,中括号内是否有值:
array := [...]string{"a", "b", "c"}
,slice := []string{"a", "b"}
。如果两个切片共享同一个底层数组的话,一个修改元素会影响到另一个切片可以用
make
函数进行初始化make([]int, 3)
,也可以以数组进行切片array[1, 2]
。以数组声明切片时,可以指定两个或三个索引,分别是“起始,长度” && “起始,长度,容量”。为了减少内存分配次数以提升性能,你需要思考如何传递make的参数Go不允许为简单的内置类型添加方法,可用
type
创建别名实现方法重载type T Cls
创建了全新的类型,除了结构相同,和原类型无任何关系(无原类型的方法集、无法相互类型断言…etc.),当然,结构相同是可以做强制类型转换的Go1.9引入了type alias,
type T = Cls
,除了名字不同,和原类型无任何区别(拥有原类型的方法集、可以在任何地方将Cls替换为T…etc.)Go语言的OOP与一般语言不同,它的多态是以接口实现的,继承是以内嵌实现的
嵌入后外部类型相当于子类,内部类型相当于父类,内部类型的方法实现会被提升到外部类型
结构体中类型的后面可使用raw string声明标签,它是提供每个字段元信息的一种机制,如使用
encoding/json
库解析json时,将json文档与结构体字段进行映射:type jsonResult{ URL string `json:"url"` title string `json:"title"` }
将对象实例赋值给接口时,要求实例实现了接口的所有方法。编译器会根据需求生成新的方法,当使用下面一种方法赋值时,编译器无法通过,原因是当将int实例的指针赋值给接口时,编译器会自动生成新的对象方法
func (a *myint) Less(b myint) bool {...}
,而假如将int实例的值赋值给接口,编译器不会为Add
方法生成值方法,因为这与需求违背,修改值的副本无法达成需求,故编译器会认为实例没有实现接口的所有方法集,将会报错:type myint int type mynum interface{ Less(b myint) bool Add(b myint) } func (a myint) Less(b myint) bool { return a < b } func (a *myint) Add(b myint) { (*a) += b } //将实例赋值给接口时: var a myint = 3 var b mynum = &a //var b mynum = a /*this is wrong*/
将接口赋值给接口时,可将父集赋值给子集
并发/并行
当一个函数创建为goroutine时,编译器会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。调度器会将操作系统的线程与逻辑处理器绑定,调度器在任何时间,都会控制goroutine在哪个逻辑处理器上运行。Go语言运行时默认会为每个可用的物理处理器分配一个逻辑处理器。调度器对可创建的逻辑处理器数量没有限制
Go的并发模型来自一个叫做通信顺序进程(Communicating Sequential Process, CSP)的范型。它是一种消息传递模型,故Go的并发模型是建立在信号通信而不是互斥锁
当goroutine阻塞时,调度器会创建一个新线程,并将其绑定到该逻辑处理器上,接着从本地队列里选择另一个goroutine来运行
如果希望Go程序并行运行,需要创建多于一个的逻辑处理器。这会使用多个线程,达到真正的并行效果
runtime.GOMAXPROCS(runtime.NumCPU())
分配CPU核数个逻辑处理器,runtime.NumCPU
相当于Python中multiprocessing库的cpu_count
函数获取的CPU核数runtime.Gosched()
函数强制退出当前进程,返回队列go build -race
竞争检测器保证线程安全:
原子函数
sync/atomic
库的LoadInt64
,AddInt64
及StoreInt64
函数(在x86下使用XXInt64时需要开发者自己保证内存对齐,否则panic)互斥锁
sync.Mutex
类型,对象方法为Lock()/Unlock()
,类似Python中的acquire()/release()
通道
channel
无缓冲通道
make(chan int)
,必需sender和reciever都准备好,否则阻塞有缓冲通道
make(chan int, 8)
,原生的并发安全队列。对有缓冲通道,当通道关闭后依然可以取出其中数据,但不可以再写入
channel的三种方向:
ch := make([chan | <- chan | chan <-] type)
chan:双向
<- chan int: 单向发送:
<- ch
chan <- int: 单向接收:
ch <- a[type:int]
select I/O多路复用:
//定时
select {
case data := <-ch:
process(data)
case <-time.After(time.Second * 3):
fmt.Println("time out")
}
//轮询
select {
case i1 = <- c1:
fmt.Printf("received ", i1)
case c2 <- i2:
fmt.Printf("sent ", i2)
case i3, ok := (<-c3):
if ok {
fmt.Printf("received ", i3)
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
第一个Go程序——淘宝商品爬虫
Go查文档真的很方便,文档随编译器一起下载到了本地
程序里对于正则表达式匹配中文,
[u4e00-u9fa5]
无效,只好匹配了所有字符哈希表使用前需初始化
defer
函数虽然是最后执行,但依然受到变量作用域限制,它的声明需要在引用的参数之后
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"sync"
)
type restring string
//爬取深度
const MAXDEPTH int = 10
//并发调度器
var wg sync.WaitGroup
//存储页面HTML的哈希表
var result map[int]restring = make(map[int]restring)
func request_html(goods string, index int) {
defer wg.Done()
index_url := "https://s.taobao.com/search?q=" + goods + "&s=" + strconv.Itoa(index)
req, err := http.Get(index_url)
defer req.Body.Close()
if err != nil {
fmt.Println("[*]Error")
os.Exit(-1)
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
fmt.Println("[*]Error")
os.Exit(-1)
}
result[index/44] = restring(body)
}
func (this restring) re_match() {
//fmt.Println(this)
//正则匹配页面信息
re_title := regexp.MustCompile(`"raw_title":".+?"`)
re_price := regexp.MustCompile(`"view_price":"[0-9.]+"`)
re_nick := regexp.MustCompile(`"nick":".+?"`)
re_loc := regexp.MustCompile(`"item_loc":".+?"`)
title_slice := re_title.FindAllString(string(this), -1)
price_slice := re_price.FindAllString(string(this), -1)
nick_slice := re_nick.FindAllString(string(this), -1)
loc_slice := re_loc.FindAllString(string(this), -1)
for i := 0; i < len(title_slice); i++ {
title_slice[i] = strings.Replace(strings.Split(title_slice[i], ":")[1], `"`, "", -1)
price_slice[i] = strings.Replace(strings.Split(price_slice[i], ":")[1], `"`, "", -1)
nick_slice[i] = strings.Replace(strings.Split(nick_slice[i], ":")[1], `"`, "", -1)
loc_slice[i] = strings.Replace(strings.Split(loc_slice[i], ":")[1], `"`, "", -1)
fmt.Printf("%-40v\t%-6v\t%-12v\t%-10v\n", title_slice[i], price_slice[i], nick_slice[i], loc_slice[i])
}
}
func main() {
wg.Add(MAXDEPTH)
goods := "DICKES"
for i := 0; i < MAXDEPTH; i++ {
//并发爬取页面
go request_html(goods, i*44)
}
wg.Wait()
fmt.Printf("%-40v\t%-6v\t%-12v\t%-10v\n", "商品名", "价格", "商家名称", "所在地")
fmt.Println("----------------------------------------------------------------------------------------------------------------")
for _, value := range result {
value.re_match()
}
}
2018/9/28
Go
的Scanf
函数由于一些历史遗留问题,windows
下需要添加\n
,否则会出现第二条Scanf
被\r\n
干扰的情况。具体参见(issue)[https://github.com/golang/go/issues/23562]
Go
实现清屏:
fmt.Println("\033[2J")
OR
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
err := cmd.Run()