golang关注点

Overview

之前一直是JVM系(Scala, Java)的爱好者,因为GC,因为大数据领域大部分都是JVM系。但是网上关于Golang(go)的讨论好多,天生的并发支持,Google支持,后端服务器利器等,都蛮吸引的,并且go没有C++的那些内存free,即go也是GC的,所以来学习一下 :grin:

why go? explained by Grab,

  • Go 的并发模型特别适合打车类业务。打车业务的特点是并发度高,单一任务(一次打车)持续时间长,Goroutine 非常适合编写这类程序,它的机制允许工程师以最自然的思维模式写业务代码
  • Go 是面向工程的语言。它的很多特性都特别适合大团队使用,比如强制统一的 gofmt 格式标准,避免了一切美学方面的争论。再比如内嵌的测试框架,-race 并发冲突检测,只支持源代码引用等等特性,从一开始就考虑到了在大团队大规模软件开发活动中的应用

环境安装

因为之前一直用IDEA,所以这次也是,直接使用go插件

go插件

go插件

插件安装之后,需要安装go SDK,目前机器上的SDK有Java8,Python2,其中Scala没有安装SDK,而是通过maven dependence的方式注入的,我比较喜欢这种方式,干净无侵入。

但是我目前还没找到go是否有这种方式,所以暂时按照常规方式来走,即安装go sdk,我这里安装go sdk是通过IDEA的默认提醒,跟到go官网下载然后安装一致,可能差异是一些bin path吧。然后需要在terminal下面运行go,可以把go bin append到path下再source,就可以用了。

go sdk

go sdk与new project

之后就可以new go project了,然后运行hello world。在运行go之前,需要配置用户空间GOPATH,类似Python的environment,可以是global level的,也可以是project level的,看具体项目需求了。

go hello world

go hello world

如果要单独隔离环境,可以点击new Go Modules(vgo),即versioned go。然后在go.modgo.sum里面管理包。

不过有时候go插件import github之后,即go get -t xxx之后,包名依然是红色,显示没有导入,也不可以使用点号联想来查看方法和变量。我觉得是该插件的一个bug,此时可以通过reopen project来workaround。

image

import github bug

image

reopen to fix

Key

  • 函数,结构体,方法,接口(方法和接口都是在结构体之上所构建的函数)
  • 传参中传值传地址/传引用的区别
    • 传值相当于前面的copy,在另外的函数内对该结构的修改不会影响到原生结构的值(array是传值,而slice是传地址)
    • slice、map、channel、pointers、functions默认就是引用类型,无需func sum(sli []int) {}这样做
    • Mutex is a value type, not reference type, 所以在方法当中要传*Obj,否则锁失效,因为传值重新new一个锁了,不是用obj原来的锁

Point

因为这一part是个人一直在持续修补整理的,所以可能比较分散,如果想要更清晰的了解基本用法,可以参考文件7。

作用域

- public,当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:`Group1`
- protected,标识符以小写字母开头,如:`group2`

变量初始化

可以用:=来替代var

func main() {
    // 基本类型
    var a, b, c = 1, 2, 3 // 并行初始化
    d, e, f := 1, 2, 3

    // 数组, goLang slice 和 array区别, https://segmentfault.com/a/1190000013148775
	arr := [...]int{1, 2, 3, 4, 5, 6} // 数组
	data := []int{0, 1, 2, 3, 4, 5}[:] // 切片slice
	fmt.Println(reflect.TypeOf(arr))
	fmt.Println(reflect.TypeOf(data))

    // 字典
	map := map[string]float32{"C": 5, "Go": 4.5, "Python": 4.5, "C++": 2}

    // 结构体
	type person struct {
		name   string
		age    int
		gender string
	}
	x := person{name: "Bob", age: 42, gender: "Male"}
	y := &person{name: "Bob", age: 42, gender: "Male"} // 指针
	fmt.Println(x.name)
	fmt.Println((*y).name)
	fmt.Println(y.name) // 可以直接这样取值,隐式间接引用
}

指针与地址

  • 结构体是有类型,不同变量的集合
    • *表示取该地址的值,&表示取该值的地址
  • 方法是一种带有接受器的特殊函数。接收器可以是值或者指针
  • 接口是方法的集合,接口有助于将同类型的属性组合起来
func main() {
    var a = 4
    var ptr = &a
    println("a的值为", a)     // 4
    println("*ptr为", *ptr)  // 4
    println("ptr为", ptr)    // 0xc000032778
}

func main() {
	type Person struct {
		name string
		age  int
		addr string
	}

	type Employee struct {
		*Person // 匿名字段
		salary int
		code   int
		addr   string
	}

	em1 := &Employee{
		Person: &Person{
			name: "mary",
			age:  23,
			addr: "beijing",
		},
		salary: 5000,
		code:   100,
		addr:   "shenzhen"}
	em2 := em1.Person

	fmt.Println(em1)         // &{0xc0000941b0 5000 100 shenzhen}
	fmt.Println(em2)         // &{mary 23 beijing}
	fmt.Println((*em2).name) // mary
}

支持多种匹配case

func main() {
    s1 := 3
    s1 = 4
    switch s1 {
    case 1, 2, 3, 4:
        println(1)
    default:
        println(2)
    }
}

循环与条件

func main() {
    var i, j int
    for i = 1; i < 10; i++ {
        for j = 1; j <= i; j++ {
            if i == j {
                fmt.Printf("平方和,%dx%d=%d\n", i, j, i*j);
            }
        }
    }
}

for i, n := 0, length(s); i < n; i++ { 	// 避免多次调用length函数
    println(i, s[i])
}

闭包

func main() {
    add_func := add(1, 2)
    fmt.Println(add_func(1, 1))
    fmt.Println(add_func(0, 0))
    fmt.Println(add_func(2, 2))
}

// 闭包使用方法
func add(x1, x2 int) func(x3 int, x4 int) (int, int, int) {
    i := 0 // global
    return func(x3 int, x4 int) (int, int, int) {
        i++
        return i, x1 + x2, x3 + x4
    }
}
// 1 3 2
// 2 3 0
// 3 3 4


// ************************************************************************************
func main() {
	TwoAdder := Adder(1)     // 得到一个funcA,x=1
	fmt.Println(TwoAdder(3)) // 将入参3放入funcA,得到
}

func Adder(x int) (func(y int) int) {
	return func(b int) int {
		return x + b
	}
}

闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。

func main() {
    for i := 0; i < 3; i++ {
        defer func(){ println(i) } ()
    }
}
// Output:
// 3
// 3
// 3

递归

// 逆着来
// time O(2^n), space O(n)
func fibonacci(n int) int {
    if n < 2 {
        return n
    }
    return fibonacci(n-2) + fibonacci(n-1)
}

// 顺着来
// time O(logN), space O(1)
func fibonacci2(n int) int {
    if n == 0 {
        return 0
    }

    f0, f1 := 0, 1
    for n > 0 {
        n -= 1
        f2 := f1 + f0
        f0 = f1
        f1 = f2
    }

    return f0
}

func main() {
    fmt.Println(fibonacci(14)) // 377
    fmt.Println(fibonacci2(14))
}

类型转换

func main() {
    var sum, count = 17, 5

    add := sum + count
    mean2 := sum / count
    mean := float32(sum) / float32(count)

    fmt.Println(add)   // 22
    fmt.Println(mean2) // 3
    fmt.Println(mean)  // 3.4
}

并发

  • 并发concurrency:切换时间片,类似时分复用,比并行优秀
  • 并行parallelism:利用计算机多核实现多线程

  • channel要点
    1. channel
    2. buffer chan
    3. close chan

c <- “chenfh5” // write to chan <- c // fetch from chan, if no data then wait (sync)

有缓存是异步的 没缓存是同步的

select multi chan

  • 类似switch,但是select只用于channel
  • select只是一次运行,如果要做成类似生产者消费者,需要加入while true
  • go语言的case语句不需要break关键字去跳出select
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s, (i+1)*100)
    }
}
func say2(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(1500 * time.Millisecond)
        fmt.Println(s, (i+1)*150)
    }
}

func main() {
    go say2("ccccc") // c是多线程, 一个go关键字就能立刻起一个新线程

    // a和b是顺序执行(但是由于c的等待时间过长,这里main主线程结束了,c还没开始第一轮)
    // 如果要实现类似java的join的话,可以用channel
    say("aaaaa")
    say("bbbbb")
}

// ************************************************************************************
// Join
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s, (i+1)*100)
    }
}
func say2(s string, ch chan int) {
    for i := 0; i < 5; i++ {
        time.Sleep(1500 * time.Millisecond)
        fmt.Println(s, (i+1)*150)
    }
    ch <- -1 // 主动给一个值,则主线程会一直等待信道中的值
    close(ch)
}

func main() {
    ch := make(chan int)
    go say2("cccc", ch)

    say("aaaa")
    say("bbbb")
    fmt.Println(<-ch) // -1
}

// **************************** // WaitGroup

// 这里是2层,最好是直接1层
func thread0(wg *sync.WaitGroup, id string) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		var wg2 sync.WaitGroup
		route0(&wg2, id+"_1")
		route0(&wg2, id+"_2")
		wg2.Wait()
	}()
}

func route0(wg *sync.WaitGroup, s string) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			time.Sleep(1 * time.Millisecond)
			fmt.Println(s)
		}

	}()
}

func main() {
	// 2 level
	fmt.Println("begin")
	var wg1 sync.WaitGroup
	thread0(&wg1, "a")
	thread0(&wg1, "b")
	wg1.Wait()
	fmt.Println("end1")

	// 1 level
	fmt.Println("begin")
	var wg sync.WaitGroup
	route0(&wg, "z")
	route0(&wg, "y")
	route0(&wg, "x")
	route0(&wg, "w")
	wg.Wait()
	fmt.Println("end2")
}

与Scala的对比

这里go是一类static语言,而scala更像一类面向对象语言,看下面的判断字符串包含关系代码。

func main() {
	full, part := "chenfh5", "chen"
	is_contains := strings.Contains(full, part)
	fmt.Println(isContains)
}
  def main(args: Array[String]): Unit = {
    val (full, part) = ("chenfh5", "chen")
    val isContains = full.contains(part)
    println(isContains)
  }

go更类似于调用一个静态方法(scala的object方法),而scala则是实例的成员函数。 比如说go中处理字符的unicode包,处理字符串的strings包

读写文件

// read
func main() {
    inputFile, inputError := os.Open("input.dat")
    if inputError != nil {
        fmt.Printf("An error occurred on opening the inputfile\n")
        return // exit the function on error
    }
    defer inputFile.Close()

    inputReader := bufio.NewReader(inputFile)
    for {
        inputString, readerError := inputReader.ReadString('\n')
        fmt.Printf("The input was: %s", inputString)
        if readerError == io.EOF {
            return
        }      
    }
}

// ************************************************************************************
// write
func main () {
	outputFile, outputError := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666)
	if outputError != nil {
		fmt.Printf("An error occurred with file opening or creation\n")
		return  
	}
	defer outputFile.Close()

	outputWriter := bufio.NewWriter(outputFile)
    outputWriter.WriteString("hello world!\n")
	outputWriter.Flush()
}

// ************************************************************************************
// copy
func main() {
	CopyFile("target.txt", "source.txt")
	fmt.Println("Copy done!")
}

func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	defer src.Close()

	dst, err := os.Create(dstName)
	if err != nil {
		return
	}
	defer dst.Close()

	return io.Copy(dst, src)
}

main入参

if len(os.Args) > 1
os.Args[0]是程序名从1开始才是入参

future并发

// 普通流水线模式,先计算a_inv,再计算b_inv
func InverseProduct(a Matrix, b Matrix) {x
    a_inv := Calc(a)
    b_inv := Calc(b)
    return Product(a_inv, b_inv)
}

// ************************************************************************************
// a和b同时计算
func InverseProduct(a Matrix, b Matrix) {
    a_inv_future := CalcFuture(a)   // start as a goroutine
    b_inv_future := CalcFuture(b)   // start as a goroutine
    a_inv := <-a_inv_future // 等到a_inv的计算完成
    b_inv := <-b_inv_future // 等到b_inv的计算完成
    return Product(a_inv, b_inv)
}

func InverseFuture(a Matrix) chan Matrix {
    future := make(chan Matrix)
    go func() {
        future <- Calc(a)
    }()
    return future
}

控制语句

if条件

左大括号要跟if在同一行 没有小括号 func main() { if a, b := 1, 2; a > 0 { a– fmt.Println(a, b) } }

switch条件

func main() {
	switch a := 1; a {
	case 0:
		fmt.Println("ashi0")
	case 1:
		fmt.Println("ashi1")
		fallthrough // 默认是命中后自动退出switch,使用fallthrough来强制执行下一段
	case 2:
		fmt.Println("ashi2")
	default:
		fmt.Println("ashix")
	}
}

循环for

func main() {
	a := []int{5, 2, 6, 3, 9}
	fmt.Println(a)

	n := len(a)
	for i := 0; i < n; i++ {
		for j := i + 1; j < n; j++ {
			if a[j] > a[i] {
				tmp := a[i]
				a[i] = a[j]
				a[j] = tmp
			}
		}
	}

	fmt.Println(a)
}

函数func

  • func (xxx 结构体) 方法名(yyy 入参) (zzz 出参) { return … }
func main() {
	/**
	  值拷贝
	  地址拷贝,然后修改地址里面的值
	*/
	a := 1
	add := &a    // 取a的地址
	A(add)       // 入参是int指针(即需要传入int变量的地址)
	aVal := *add // 取a的地址里头的值

	fmt.Println(a, add, aVal)
}

func A(add *int) {
	*add = 2 // 修改了该地址里头的值了
	fmt.Println(*add)
}
type person struct {
	Name string
	Age  int
}

func main() {
	a := person{
		Name: "joe",
		Age:  9,
	}
	b := person{
		Name: "may",
		Age:  8,
	}
	fmt.Println(a)
	A(a) // 值传递,如果在函数内修改了a,将不改变传递前a的值
	fmt.Println(a)

	fmt.Println(b)
	B(&b) // 地址传递,main和函数B是共享该地址的值的,所以任意一个修改了,2个都可见
	fmt.Println(b)
}

func A(per person) {
	per.Age = 11
	fmt.Println("A", per)
}

func B(per *person) {
	per.Age = 22
	fmt.Println("B", per)
}

结构体struct

  • type关键字
  • struct关键字
  • 当使用结构嵌入时,不能带alias命名 ```go type human struct { Sex int }

type student struct { //Human human // can not specific here human Name string Age int }

func main() { a := student{ Name: “zhangsan”, // 字面值初始化 Age: 10, human: human{Sex: 0}, }

fmt.Println(a)
a.Age = 2
a.Sex = 1 // 直接嵌套
fmt.Println(a) } ```

方法method

  • 属于哪一个结构体的方法
  • 相比普通函数,多了一个结构体receiver ```go // normal func func f(a, b int) (int, string) { return a + b, strconv.Itoa(a) + “+” + strconv.Itoa(b) }

// 方法体,类似于重载,但属于struct的method type A struct { Name string }

type B struct { Name string }

func main() { a, b := 1, 2 x, y := f(a, b) fmt.Println(x, y)

// f0是从属于结构体的方法,因为f0的函数签名有结构体作为receiver
m, n := A{"A"}, B{"B"}
fmt.Println(m, n)
fmt.Println(m.f0(), n.f0())
fmt.Println(m, n) // m是AA而不是A了,因为a出入的是指针 }

func f(a, b int) (int, string) { return a + b, strconv.Itoa(a) + “+” + strconv.Itoa(b) }

// 指针结构体作为receiver func (a *A) f0() string { a.Name = “AA” return a.Name }

func (b B) f0() string { b.Name = “BB” return b.Name }

// normal function func MultiPly3Nums(a int, b int, c int) int { return a * b * c }

// with type type Square struct { side float32 }

func (sq *Square) Area() float32 { return sq.side * sq.side }


## 接口interface
- type关键字
- interface关键字
- 结构体要实现接口里的`所有`方法才算实现了该接口
```go
type USB interface {
	Name() string
	Connector
}

type Connector interface {
	Connect()
}

type PhoneConnecter struct {
	name string
}

// PhoneConnecter实现了USB的全部接口
func (pc PhoneConnecter) Name() string {
	return pc.name
}

func (pc PhoneConnecter) Connect() {
	fmt.Println("connect: ", pc.name)
}

func main() {
	var a USB
	a = PhoneConnecter{name: "i am PhoneConnecter"}
	a.Connect()
	Disconnect(a)
}

// 类型断言
func Disconnect(usb USB) {
	// method 1, ok pattern
	if pc, ok := usb.(PhoneConnecter); ok { // ok pattern, 解开usb,嵌入接口(与嵌入结构类似)
		fmt.Println("disconnect: ", pc.name)
		return
	}
	fmt.Println("Unknown device.")

	// method 2, type switch
	switch v := usb.(type) {
	case PhoneConnecter:
		fmt.Println("disconnect: ", v.name)
	default:
		fmt.Println("Unknown device.")
	}

}

反射reflect

type User struct {
	Id   int
	Name string
	Age  int
}

func (u User) Hello() {
	fmt.Println("hello world")
}

func Info(o interface{}) {
	t := reflect.TypeOf(o)
	fmt.Printf("Type: %v\n", t.Name())

	v := reflect.ValueOf(o)
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		value := v.Field(i)
		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	for i := 0; i < t.NumMethod(); i++ {
		m := t.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}

func main() {
	u := User{1, "OK", 2}
	Info(u)
}

catch panic

func main() {
	f()
}

func f() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f", r)
		}
	}()
	errFunc()
}

func errFunc() {
	panic([]int{1, 2, 3})
} 

nil声明

当仅声明某变量是指针,而不对其进行初始化,那么该变量是nil,可以用这个方式来进行nil判断。

var mlFeature *remllstream.MLFeature // 声明
if remllog.IsValidRolloutRate(mapID, 0) {
    mlFeature = &remllstream.MLFeature{MapVersion: mapVersion} // 初始化 
}

if mlFeature != nil { // 判断
    // do some thing
}
package main

import "fmt"

type Person struct {
	ID    int
	Nname string
	Age   int
	Addr  string
}

func main() {
	var p0 *Person // 声明
	var v0 Person // 声明
	var p1, v1 = &Person{}, Person{} // 声明并初始化
	p2, v2 := &Person{}, Person{} // 声明并初始化

	fmt.Println(fmt.Sprintf("p0=%+v", p0)) // p0=<nil> 指针的var形式声明在未赋值前是nil
	fmt.Println(fmt.Sprintf("v0=%+v", v0)) // v0={ID:0 Nname: Age:0 Addr:}
	fmt.Println(fmt.Sprintf("p1=%+v", p1)) // p1=&{ID:0 Nname: Age:0 Addr:}
	fmt.Println(fmt.Sprintf("v1=%+v", v1)) // v1={ID:0 Nname: Age:0 Addr:}
	fmt.Println(fmt.Sprintf("p2=%+v", p2)) // p2=&{ID:0 Nname: Age:0 Addr:}
	fmt.Println(fmt.Sprintf("v2=%+v", v2)) // v2={ID:0 Nname: Age:0 Addr:}
}

Sprint vs Sprintf

Sprint一般不用,而常用Sprintf

func main() {
	const name, age = "Kim", 22
	s1 := fmt.Sprintf("%s is %d years old", name, age) // 占位符方式
	s2 := fmt.Sprint(name, " is ", age, " years old") // 平铺直叙方式
	fmt.Println(s1)
	fmt.Println(s2)
}

全局变量 vs 变量的方法

  • 如果在一个.go文件中定义了一个var,那么这个var可以当作是全局变量
      var (
          mu      sync.Mutex // guards balance
          balance int
      )
        
      func Deposit(amount int) {
          mu.Lock()
          balance = balance + amount
          mu.Unlock()
      }
        
      func Balance() int {
          mu.Lock()
          b := balance
          mu.Unlock()
          return b
      }
    
  • 如果是声明并初始化了一个type,而该type里面有方法,这样type就可以有很多个,而各个type之间的方法调用互不干扰,要注意的是锁里面的方法要传地址,不能传值,因为Mutex是值类型
      type Container struct {
          sync.Mutex // guards counters,可以不指定名,这样mutex的方法会自动适配到obj当中
          counters map[string]int
      }
        
      // Wrong
      func (c Container) inc(name string) {
        c.Lock()
        defer c.Unlock()
        c.counters[name]++
      }
        
      // Correct
      func (c *Container) inc(name string) {
        c.Lock()
        defer c.Unlock()
        c.counters[name]++
      }
    

单例

type Person struct { // global person
	Id   int64
	Name string
	Age  int32
}

var obj *Person // private, but can get from GetInstance()
var doOnce sync.Once

func GetInstance() *Person {
	doOnce.Do(func() {
		obj = &Person{}
	})
	return obj
}

并发

常见的并发模式,

  • sync.Mutex/ sync.
  • 生产者消费者模型,总共只有一条chan,可以有多个producer往chan里写,也可以有多个consumer从chan里读
  • 发布订阅模型,每一个订阅者都有一条自己的chan,发布者每发布一条消息都会遍历其所有订阅者,并根据topicFunc来挨个判断是否向其chan发出消息
  • 控制并发数,gatefs包
  • 赢者为王,安全退出,同一时间派发多个任务,最先完成的为王,并且退出其余任务(closeChan->WaitGroup, context.cancel->WaitGroup)
    select {
    case sub <- v: // do something()
      fmt.Println("write v to subChan")
    case <-ctx.Done():
      return // context退出(更优雅)
    case <-cannelChan:
      return // 从cannelChan退出
    default:
      fmt.Println("hello")
    }
    

基于channel的通信

  • 在无缓存的chan上的每一次发送操作都有与其对应的接收操作。发送与接收发生在不同的goroutine上,而如果发生在同一个goroutine上,那么就会导致死锁

Reference

  1. Go语言教程
  2. Go入门指南
  3. The Go Programming Language Documentation
  4. Difference Between Go vs Scala
  5. goland ide 对于 go 1.11的配置
  6. GO语言零基础从入门到精通-入门
  7. 也许是最简洁版本,一篇文章上手Go语言
  8. Go指南