字节跳动第五届青训营后端方向笔记 Day 1

·

5 min read

本文不是教程,也尽量不废话;

本文更多记录的是课堂过程中的想法和洞见以及课堂上没讲到的内容的补充,如果你也参加了这一届活动,也许对你能有所帮助;

几乎不会照搬 PPT ;

个人感觉基础课程不如 A Tour of Go ;

如果你在寻找教程,我建议看 A Tour of Go 。看完后读 Golang 圣经。

1 简介

1.1 什么是 Go 语言

  • Golang = C 语言 + GC + 协程 + 反射 + interface;
  • 不像一些别的语言,Go 的 http 服务器 / 客户端是由标准库实现的,所以上手门槛更低,稳定性更强,同时还能享受标准库随编程语言持续迭代带来的性能优化;
  • 自带完善的工具链,再也不需要 gradle / maven / junit / eslint / prettier / mocha ...;
  • 支持的目标平台多,支持交叉编译;
  • 快;
  • ……

1.2 谁在用 Go

除了视频提到的宝贝以外,搞云原生的几乎都在用,CNCF 基金会下基本上都是 Go 的项目。

1.3 字节为什么用 Go

Go 现在已经成为了字节跳动内部使用率最高的程序设计语言。

  1. Python 润起来太慢
  2. C++ 写起来太慢
  3. 不喜欢 Java
  4. 部署简单,学习成本低,依赖简单
  5. 内部 RPC 和 HTTP 框架的推广

2 速通 Go

2.1 开发环境

配就完事了,这还要教?

  • 建议用 Goland ,血压比较稳定;
  • 家里没有无限矿业让你订阅 Intellij 的可以使用 VSCode;
  • 同样推荐使用在线开发环境 Gitpod 来学习。

2.2 基础语法 - Hello World & 变量

  • Hello World 不用学,我早在 114514 年前就掌握了若干门语言的 Hello World 语法;
  • C 语言基础类型 + Java 字符串;
  • :=const 可以自动推断类型;
  • 支持科学计数法如 const i = 3e20 / h

2.3 基础语法 - if

  • bool 表达式不需要括号;

    Go 去掉了大部分控制语句中需要使用表达式的地方的括号,如 if, for, switch 等。

  • 必须使用大括号。

2.4 基础语法 - 循环

C 循环加一些特性:

  • 可以使用 for {} 来做死循环;
  • 可以直接当 C while 用。

2.5 基础语法 - switch

  • 看起来像 C switch,但不用 break;
  • 可以用任何类型,本质上是 if - else 语句的序列,即 case 可以不用常量。一个不太符合传统 switch 的使用案例:
    func main() {
        fmt.Println("When's Saturday?")
        today := time.Now().Weekday()
        switch time.Saturday {
        case today + 0:
            fmt.Println("Today.")
        case today + 1:
            fmt.Println("Tomorrow.")
        case today + 2:
            fmt.Println("In two days.")
        case today + 3:
            fmt.Println("In three days.")
        case today + 4:
            fmt.Println("In four days.")
        default:
            fmt.Println("Too far away.")
        }
    }
    
  • 可以不适用 condition 表达式,可以用来美化一个 if - else 序列:
    func main() {
        t := time.Now()
        switch {
        case t.Hour() < 12:
            fmt.Println("Good morning!")
        case t.Hour() < 17:
            fmt.Println("Good afternoon.")
        default:
            fmt.Println("Good evening.")
        }
    }
    

Tips: 控制语句可以加一个短的变量声明

if, switch 都可以加一个短的变量声明,这个操作其实来源于传统 for 中,的 init statement ,例如:

if v := math.Pow(x, n); v < lim {
    return v
}

switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("OS X.")
case "linux":
    fmt.Println("Linux.")
default:
    // freebsd, openbsd,
    // plan9, windows...
    fmt.Printf("%s.\n", os)
}

2.6 基础语法 - 数组

就是纯数组,写业务通常用切片。

2.7 基础语法 - 切片

可变长度的数组实际上更像是数组的引用,slice 本身并不存储数据,他只是描述数组的一个部分。改变 slice 中的一个值,将会改变它描述数组的数据,所以其他描述这个数组的 slice 也会发生改变。

Cheatsheet: slice 的四种写法

  • primes := [6]int{2, 3, 5, 7, 11, 13} // array literal
    
    var s []int = primes[1:4]
    
  • primes := []int{2, 3, 5, 7, 11, 13} // slice literal

    • []带初始 capacity 的是 array,不带的是 slice
  • s := make([]string, 3, "nb") // make built-in function
  • // slice literal + struct literal
    s := []struct {
        i int
        b bool
    }{
        {2, true},
        {3, false},
        {5, true},
        {7, true},
        {11, false},
        {13, true},
    }
    
  • board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }
    

2.8 基础语法 - map

使用 make 函数创建:

m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
    40.68433, -74.39967,
}

使用 map literal :

var m = map[string]Vertex{
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

或省略类型名称

var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

CRUD:

  • C/U: m[key] = elem
  • R: elem = m[key] or elem, ok = m[key]
  • D delete(m, key)

遍历一个 map 的顺序是完全随机的

2.9 基础语法 - range

相当于创建一个 enumerate iterator ,可以针对 slice 和 map 进行遍历。

2.10 基础语法 - 函数

经典 if err != nil 时刻。

2.11 基础语法 - 指针

函数默认是 pass by value 的,想要 pass by reference 就得用指针。

2.12 基础语法 - 结构体

用结构体的指针,同样也可以直接访问到结构体内的变量,不需要专门解引用,如:

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    p := &v
    p.X = 1e9
    fmt.Println(v)
}

结构体同样也支持 literal:

type Vertex struct {
    X, Y int
}

var (
    v1 = Vertex{1, 2}  // has type Vertex
    v2 = Vertex{X: 1}  // Y:0 is implicit
    v3 = Vertex{}      // X:0 and Y:0
    p  = &Vertex{1, 2} // has type *Vertex
)

2.13 基础语法 - 方法

Go 没有,但是你可以给类型绑定函数,这个绑定上去的函数就是方法。

其实对于非结构体的类型,也可以定义方法,这一个特性有点像扩展函数:

但是必须要求这个类型声明和方法的声明在同一文件内。

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

此处相同,调用一个方法时,也可以直接使用指针调用,方法接收到是值还是指针,由方法的声明决定,例如:

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

对于上述方法,以下两种调用方式是等价的:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

此处同样可以理解为,由于在 Scale 的函数声明中,制定了接收 Vertex 的指针(也就是*Vertex),所以 go 会把 v.Scale(5) 解释成 (&v).Scale(5)

2.14 基础语法 - 错误处理

Go 的错误处理让人感觉不便可能是由于语法臃肿,但在工程实践中,我发现思考一个错误应该由谁来处理,对我们编写程序帮助很大。

2.15 基础语法 - 字符串处理

用时查表。

2.16 基础语法 - 字符串格式化

如果你用 GoLand,可以在一个 f 方法(如 Printf,Sprintf)内的字符串按下 alt + enter 来快速插入一个变量,百试不爽。

2.17 基础语法 - JSON 处理

有标准库,函数名和别的语言有区别,在 Go 中我们通常用 MarshalUnmarshal 做字符串和 struct 之间的转换,但其实 Go 也是有 EncodeDecode 的,它们的区别在于,如果你的结果是字符串,那就用 Marshal ,如果你的结果需要 Write 给一个 Writer ,那就用 EncodeUnmarshalDecode 同理。

Summarized from this article.

2.18 基础语法 - 时间处理

又一个 Go over TypeScript 的理由,用的时候查表完事。

2.19 基础语法 - 数字解析

strconv.Parse${要啥写啥} 即可。

2.20 基础语法 - 进程信息

写 cli 建议用 Cobra

3 实战练习

3.1 猜数字

典中典猜数字生,成随机数一定要设置种子。

3.2 在线词典

HTTP Web API 的 CLI 封装。都是基操。

但是现在 ReadAll 不需要 ioutil 了,直接 io.ReadAll 就完事,参考 https://github.com/nicognaW/xianrail_exporter/blob/dev/api/xianrail/passenger.go

原来字节员工也用 json 2 go / go 2 json 这种 codegen 啊,那没事了

3.3 Socks5 代理

学一波。

作业

  1. 就是你填 format verbs 进去,他会按你填的格式把变量弄出来,比如
    var guess int
    fmt.Scanf("%d", &guess)
    biz(guess)
    
  2. 这个有点过于基操了,看看 https://github.com/nicognaW/xianrail_exporter/blob/dev/api/xianrail/passenger.go
  3. 同时开两个 goroutine 完事,加个 channel 用来做等两个 goroutine 都跑完了。
    ch := make(chan bool)
    go func(){
        check(ch)
    }()
    <-ch