Overview
之前一直是JVM系(Scala, Java)的爱好者,因为GC,因为大数据领域大部分都是JVM系。但是网上关于Golang(go)的讨论好多,天生的并发支持,Google支持,后端服务器利器等,都蛮吸引的,并且go没有C++的那些内存free,即go也是GC的,所以来学习一下
why go? explained by Grab,
- Go 的并发模型特别适合打车类业务。打车业务的特点是并发度高,单一任务(一次打车)持续时间长,Goroutine 非常适合编写这类程序,它的机制允许工程师以最自然的思维模式写业务代码
- Go 是
面向工程的语言
。它的很多特性都特别适合大团队使用,比如强制统一的 gofmt 格式标准,避免了一切美学方面的争论。再比如内嵌的测试框架,-race 并发冲突检测,只支持源代码引用等等特性,从一开始就考虑到了在大团队大规模软件开发活动中的应用
环境安装
因为之前一直用IDEA,所以这次也是,直接使用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与new project
之后就可以new go project了,然后运行hello world
。在运行go之前,需要配置用户空间GOPATH,类似Python的environment,可以是global level的,也可以是project level的,看具体项目需求了。
go hello world
如果要单独隔离环境,可以点击new Go Modules(vgo),即versioned go。然后在go.mod
和go.sum
里面管理包。
不过有时候go插件import github之后,即go get -t xxx
之后,包名依然是红色,显示没有导入,也不可以使用点号联想来查看方法和变量。我觉得是该插件的一个bug,此时可以通过reopen project来workaround。
import github bug
reopen to fix
Key
- 函数,结构体,方法,接口(方法和接口都是在结构体之上所构建的函数)
- 传参中传值与传地址/传引用的区别
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要点
- channel
- buffer chan
- 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上,那么就会导致死锁