Golang学习总结

本文总阅读量
  12 mins to read  

在用Python写程序的过程中,由于GIL的存在,CPU密集型的并发变得效率低下,且为了构建一个设计精良的并发程序需要考虑很多问题

Go语言在底层设计好了并发模型,所以它生下来就是用来写大型并发应用的,最近学习了Go语言。由于它的语法是C-like的,加上基本不存在依赖问题,有问题随时查官方文档,所以学习起来进度还是挺快的。学习结束,总结一些Tips:

2018/12/4–update: 写多了发现,Golang在整个语言设计上很平庸,以及某些语法逻辑很混乱,无泛型(这个还好,多复制粘贴几次,据说2.0要加上了),包管理很蛋疼(每次都import "github.com/xxx/xx",而且导入语句居然是字符串…),错误处理…我就懒得说了,if err:= xx; err != nil{}真鹅心


程序架构

  • 在Go中,程序是以包为单位组织的,每个文件夹为一个包,每个程序的开头会有package语句声明自己属于哪个包。会编译成二进制文件的程序会声明自己为package main

  • 包级公开变量以首字母大写作为标识,而私有变量以小写字母开头。不同于一般语言的下划线声明私有变量

  • init函数用来对程序进行初始化,它在main函数之前执行

  • 接口的方法集:

    • 类型*S的可调用方法集包含接受者为*SS的所有方法集

    • 类型S的可调用方法集只包含接受者为S的方法集,不包含接受者为*S的方法集

  • 内嵌对象的方法集提升:

    • 如果S包含一个匿名字段KS*S的方法集都包含接受者为K的方法提升

    • 如果S包含一个匿名字段*TS*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)
 }

 //c
 #include <stdio.h>
 int* foo(){
 	 long long i = 199000000000;
 	 return &i;
 }
 int main(){
	 int* pointer = foo();
	 printf("%d", *pointer);
	 return 0;
 }

  • 异常处理:
defer func() {
	if r := recover(); r != nil {
		log.Printf("Runtime error caught: %v", r)
	}
}()

语法

  • Go中的指针与值的使用非常不严格,大多时候不需要明确进行解引操作,编译器会自动生成正确的语句。重要的是引用类型和值类型,引用类型:hashmap, channel, interface, slice, function

  • 对于可迭代对象,range返回的是元素的副本

  • 对于花括号包裹的数据类型的定义,单行时只需在每个元素间用逗号分割,多行时强制每个元素后加拖尾逗号

    a := map[int]int{1: 1, 2: 2}
      
    a := map[int]int{
        1: 1,
        2: 2,
    }
    
  • Go中的闭包函数可以直接修改外层函数的变量,因为它获得的是外层变量的指针,故Go中没有类似Python3的关键字nonlocal,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相反如:

    • 变量声明: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]。以数组声明切片时,可以指定两个或三个索引,分别是“起始,长度” && “起始,长度,容量”,建议使容量==长度的切片,这样在扩容的时候会直接分配新的底层数组,避免不必要的bug

  • Go不允许为简单的内置类型添加方法,可用type创建别名实现方法重载

  • 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, AddInt64StoreInt64函数

    • 互斥锁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

GoScanf函数由于一些历史遗留问题,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()