golang笔记

一些约定

  1. 使用驼峰命名法
  2. 首字母小写的变量/函数只能在包内使用(多按一次shift我要累死了, 虽然编辑器有时在小写输入的情况下也会自动补全大写内容, 但有时不是那么聪明)

数据类型

bool

布尔, true或者false

string

字符串, 使用双引号 比如"hello world"

rune

类似于字符, 在使用for _, char := range string迭代string的时候, char的类型就是rune

int

未分int8, int16, int32等等, 可能有些api返回的是uint类型, 使用int()转化为int

byte

[]byte可以跟string互相转化, len(string)的长度实际上就是string转化为[]byte之后的长度. 每个unicode的长度为3. 比如说len("hello world")==11, 但是len("你好 世界")的长度为3+3+1=7. 每个汉字因为是unicode所以为3.

指针

可以指向其他数据类型, 在传值的时候传递指针可以避免比较复杂的数据结构被复制一份.(比如比较大的数据, 结构体等等)

数组

用于存放类型相同的一组数据.

slice

可变长度

array

长度不可变

map

类似于python中的dict, 但是需要指定key和value的类型

比如python中的Map[str, str]在golang中为map[string]string, 这个map只能以string为key, 以string为value.

如果要一个可以存储任意数据类型的map可以map[string]interface{}

函数

func main(arg1 argType1, arg2 argType2) (returnType1, returnType2){
main body
}

golang中, 变量的类型是放在变量名之后的.

struct

结构体, 类似于python中的class.

定义

type Episode struct {
Title string
Torrent string
Episode int
Time int
}

初始化

使用大括号

var e = Episode{
Title: "hello world",
Episode: 1,
}

结构体的方法

如果要在其上绑定函数, 使用

func (episode *Episode) getTitle() string {
return episode.Title
}

条用跟其他语言的调用方法类似.

e.getTitle()

接口 interface

还没有实际用到, 所以只有一些粗浅的理解.

interface接口是为了面向对象而出现的, 一个函数可以参数可以为一系列不特定的类型, 但无所谓具体的类型, 只要这个类型实现了特定的方法即可.

比如一个writeTo函数接受一个io.Writer的参数

func writeTo(w io.Writer) error {

}
// io.Writer
type Writer interface {
Write(p []byte) (n int, err error)
}

只要是实现了Write方法的, 输入值和返回值匹配([]byte) (int, error)类型都可以做为这个函数的参数. 比如说默认的标准库os提供的os.Stdout就是一个实现了io.Writer接口的类型. 比如说, 这里就可以以writeTo(os.Stdout)的形式调用.

channel

信道

go并发中, 不同goroutine的通信工具.

无缓冲区或者缓冲区已满的情况下进行写入是阻塞的.

如果信道为空, 而试图读取也会阻塞.

只有读取没有写入或者只有写入没有读取会导致死锁.

并发编程

golang提供了go关键词来开启一个goroutine.

因为goroutine函数的返回值是会被丢弃的, 使用回调函数的话又会陷入回调地域中, 所以需要一种额外的方式来接受异步函数的返回结果.

这里就要贴出那句go并发文章常常能看到的话了

不要通过共享内存来通信,而应该通过通信来共享内存

channel, 就是go提供的通信的工具. 可以在一个线程中发送数据, 在另一个线程中接收数据. 如果我们要写一个异步爬虫, 就可以开四个线程来爬取数据, 在爬到数据后通过channel发送到主线程, 然后在主线程中使用channel接受, 交给pipeline来处理.

package main

import (
"fmt"
)

func costumer(c chan int) {
for each := range c {
fmt.Printf("costume %d\n", each)
}
}

func main() {
fmt.Println("hello world")
var c = make(chan int)
go costumer(c)
for index := 0; index < 5; index++ {
c <- index
}
}

使用go costumer(channel)启动了一个新的消费者线程, 而主线程自己也是一个goroutine, 虽然跟costumer不在同一个线程中, 但是仍然可以通过channel通信, 在这里, 主线程就是生产者, 消费者模型中的生产者.

并发控制

有时候并不需要很大的并发量, 比如我们只想起4个或者8个生产者, 可以使用sync.WaitGroup来进行并发控制.

package main

import (
"fmt"
"sync"
)

func main() {
var controller = newController(4)

var urls = []string{}

for index := 0; index < 20; index++ {
urls = append(urls, fmt.Sprintf("%d", index))
}

go controller.dispatchGocoutine(urls)

for each := range controller.output {
fmt.Println("collect output " + each)
}
}

type asyncControl struct {
wrg sync.WaitGroup
output chan string
goroutineCnt chan int
}

// newController
func newController(size int) *asyncControl {
d := new(asyncControl)
d.wrg = sync.WaitGroup{}
d.output = make(chan string)
d.goroutineCnt = make(chan int, size)
return d
}
func (controller *asyncControl) dispatchGocoutine(urls []string) {
for _, url := range urls {
controller.goroutineCnt <- 0 // 限制线程数
controller.wrg.Add(1)
go controller.asyncTask(url)
}
controller.wrg.Wait() // 等待至所有分发出去的线程结束
close(controller.output)
}

func (controller *asyncControl) asyncTask(url string) {
defer func() {
<-controller.goroutineCnt
controller.wrg.Done()
}()
// some task here
controller.output <- "processed " + url
}

因为提到在缓冲区已满的情况下写入是阻塞的, 所以可以利用这一点来进行并发控制. goroutineCnt就是用来控制最大线程数的任务. 当我们试图开启一个新的线程的时候, 先向goroutineCnt中进行一次写入, 再开启一个新的任务. 而在工作进程中, 在整个函数执行完成后则从goroutineCnt中进行一次读取. 缓冲区就会空出一位来, 而进行工作分发的线程的阻塞此时就结束了, 在成功写入信道之后则会开启一个新的线程. 保持工作线程数量永远不会超过goroutineCnt的缓冲区长度.

这个sync.WaitGroup是另一个并发的工具, 因为不能确定其他goroutine什么时候才能结束, 所以使用wrk.Wait()来在分发结束后阻塞分发的线程, 在所有分发出去的线程结束后关闭信道.