Go基础详解

161 阅读12分钟

从业务角度看,Go语言已经在云计算、微服务、大数据、区块链、物联网等领域蓬勃发展。Docker、Kubernetes、lstio、etcd、prometheus几乎所有的云原生组件都是Go实现。

为什么字节跳动全部拥抱Go语言: web后端业务用C++不合适,以及早期团队非java背景,于是最开始的服务都是用python。但随着业务体量增长,python会有性能问题以及依赖库版本问题,于是转Go。 Go入门简单,开发效率高,性能优,部署简单,解决了依赖库版本问题。公司内部的基于goland的rpc和http框架诞生,随着框架推广,越来越多python服务使用goland重写。

Go的应用场景

Go的特点:

  1. 高性能高并发
  2. 语法简单易学
  3. 标准库丰富
  4. 工具链完善
  5. 静态链接
  6. 快速编译
  7. 跨平台
  8. 垃圾回收

垃圾回收机制

程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。
对于C++等早期编程语言栈上的内存由编译器管理回收,堆上的内存空间需要编程人员负责申请与释放。
在Go中栈上内存仍由编译器负责管理回收,而堆上的内存由编译器和垃圾收集器负责管理回收,给编程人员带来了极大的便利性。

  • 机制实现原理 垃圾检测算法以及垃圾回收算法是其中最重要得两个部分。 垃圾检测算法决定哪些对象是垃圾需要被回收,主要有引用计数法和三色标记法。由于,引用计数法有循环引用的问题,故大部分的语言都是使用三色标记法来检测垃圾的。
    垃圾回收算法决定如何回收内存,主要有标记清除,标记复制,标记压缩等。

GC触发时机:

  • 主动触发:runtime.GC()
  • 被动触发:定时触发、GC百分比(在下一次垃圾收集必须启动之前可以分配多少新内存的比率,默认为100)

go的垃圾回收是基于三色标记法,通过合理的使用内存屏障,大大较少了垃圾回收的STW。GC开始就将栈上所有的对象标记为黑色,不需要二次扫描,不需要STW;GC期间任何栈上新建对象均标记为黑色;被删除的对象标记为灰色;新增对象标记为灰色。结合了删除、插入写屏障各自优势。

传统垃圾回收算法:引用计数法

1698764398753.png

  • 优点:对象可以很快被回收,不会出现内存耗尽或到达阀值才回收。
  • 缺点:不能很好的处理循环引用

垃圾检测算法:三色标记法

原理简述:从必然不能被垃圾管理回收的对象(eg:栈对象、全局变量)GCROOTS列表出发,通过层层引用,GC ROOTS可以间接引用到的对象(对象可达),就不是垃圾,而GC ROOTS无法间接引用到的对象(对象不可达),就是我们需要回收的垃圾,而使用算法分析对象可不可达的过程,也被称为可达性分析。 一般GC(垃圾回收)开始时,GCROOTS中的对象会全部标记为黑色,GC OOTS引用的对象标记为灰色,其它的对象则是白色,当GC的标记过程结束以后,剩下的白色对象就是要清除的垃圾。

  • 黑色:GCROOTS可达,不能回收。该对象的所有引用对象都已经被标记(被加入灰色队列)。
  • 灰色:GCROOTS可达,不能回收。该对象引用的对象还没有被全部标记。
  • 白色:GCROOTS不可达 or 还没有被标记到,可回收

在标记阶段结束后,垃圾回收器会遍历整个堆,将所有未标记(仍然是白色的)的对象释放,并将已标记的对象恢复为白色,以备下一次垃圾回收。

  • 优点:解决了引用计数的缺点。
  • 缺点:需要 STW(stop the world),暂时停止程序运行。

STW优化及其问题和解决

  • 为了避免在 GC 的过程中,对象之间的引用关系发生新的变更,使得GC的结果发生错误(如GC过程中新增了一个引用,但是由于未扫描到该引用导致将被引用的对象清除了),停止所有正在运行的协程。

在垃圾回收的过程中,有一部分操作是必须要停止所有的用户线程,这被称为STW(stop the world)。STW时间的长短,是衡量一个垃圾回收算法好坏的一个重要因素。在Golang早期的时候,Golang的垃圾回收是串行的,所以STW时间特别长,达到几百毫秒,在后续的更新中,Golang垃圾回收进行了多次优化。Golang1.8后,STW停顿时间低于1ms。 Golang垃圾回收一般分为2个阶段,标记和清除。而在Golang早期的时候, 标记和清除都要STW,并且标记和清除都是单线程执行。

改进1:并发清理,且清理流程和用户线程一起执行。 改进2:并发标记,标记流程分为初始标记、标记、最终标记三部分。标记流程和用户协程一起执行。最终标记是为了避免浮动垃圾的产生。

如果用户协程和标记协程对同一个变量进行操作,会产生浮动垃圾(是需要处理的垃圾,但是标记算法误认为它不是垃圾)或者错误标记把有用的对象标记为垃圾。前者只是少回收一点垃圾,但是后者会导致用户的数据丢失,导致程序运行出错。

  • 垃圾对象被认为不是垃圾:(白引用黑断开)黑色不可回收对象突然被唯一GCROOTS引用断开,应该变白,但已经扫描过了,不会变白,变成了浮动垃圾,下一次CG可正常回收
  • 有用对象标记为垃圾情形:(黑引用白连接)已经扫描完的白色可以被回收对象,突然更改新增GCROOTS引用,导致本应变灰不该被删除。

为了解决上述给有用对象标记为垃圾情形给三色标记法增添了限制:

  • 强三色不变式 强三色不变式指的是,在三色标记法进行标记时,不允许黑色对象引用白色对象。
  • 弱三色不变式 弱三色不变式指的是,在三色标记法进行标记时,允许黑色对象引用白色对象,但是白色对象必须存在其他灰色对象对它的引用,或者有灰色对象对该白色对象是可达的。

三色不变式在实现上需要加入额外操作:

  • 写屏障:对变量进行赋值的时候编译器自动插入额外的操作 image.png
    1. 插入写屏障:引入新的白色对象时,就将白色对象标记为灰色,满足强三色不变式

      处于性能和实现复杂度的考虑,go对栈空间没有使用写屏障,导致新增的引用对象无法及时发现。为了保证程序正常运行,在执行清除回收前,go会执行STW重新扫描一遍栈空间。

    2. 删除写屏障:在GC过程中如果出现在引用删除,所删除的对象依旧会全部保留下来,满足满足弱三色不变式。虽然不用在此STW但是标记删除粒度比较粗,需要被删除的对象只有在下一轮GC中才会被删除。(当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色)

基础语法

Hello World

   // 该文件属于main包的一部分,main包是程序的入口包
   package main 
   // 导入标准库里的包
   // fmt包:屏幕输入输出字符串,格式化字符串
   import{ 
       "fmt"  
   }
   // main函数
   func main(){   // { 不能再单独的行上
       fmt.Println("hello world") // 单行结尾语句无需分号
                                  // 单行多语句,之间需要分号隔开
   }
  • 输出格式化
  • println
  • printf : %v匹配任何, %+v打印详细结果, %#v打印更详细结果,包含结构体的类型,字段名字和值信息
  • 变量:

Go是强类型语言,每一个变量都有它自己的变量类型。
变量类型后置

  // 常见变量类型
  s string = ""  // 字符串(允许加号拼接,等于号比较)
   c int = 1 // 整数
  f := float32(e)// 浮点型
   // 布尔型

变量的两种声明方式:

1. 使用 var  
   // 自动推导
   var a = "init"
   // 显示声明
   var a string = "init"
2. 使用 :=
   a := "init"

变量类型后置

常量:

  • 给var替换成const即可:const a string = "init"
  • Goland的常量没有确定类型,根据使用上下文自动确定类型

条件语句

  • if else
// 写法:条件不用加括号,执行体{}不能省略
if true{
    fmt.Println("OK.")
}
// 功能:允许声明变量,作用域只在条件逻辑块内
if i:= 9; i < 0 {
   fmt.Println("OK.")
}
  • switch case
// 特点:
   1. 无需break:C++分支不加break会继续跑完下面所有分支,Go不会 
          switch a{
          case 1:
              fmt.Println("a=1")
          case 2,3,4:
              fmt.Println("a=2 or a=3 or a=4")
          default:
              fmt.Println("a!=1,2,3,4")
          }
   2. 支持任意变量类型 
   3. 取代任意if elseswitch{
          case i<0:
              fmt.Println("i<0")
          default:
              fmt.Println("i>=0")
          }

循环语句

Go里面只有唯一的一种循环:for循环。

 // 三段式for循环
 for i:=0; i<9; i++ {
     fmt.Println(i)
}
// while式循环
for i<9 {
    fmt.Println(i)
    i++;
}
// 死循环
for {
    break;
}

数组

  • 长度固定且具有编号的元素序列,可以方便存取特定索引值,可以直接打印
  • 真实业务很少用到,更多用到的是切片
    // 使用var声明数组  
    var arr1 [5]int // 一维
    var arr2 [2][3]int // 二位
    // 使用:=声明数组
    arr1 := [5]int{1,2,3,4,5}
    arr2 := [2][3]int{{1,2,3},{4,5,6}}
    // 打印数组
    fmt.Println(arr1,arr2)

切片slice:

  • 可变长度的数组,操作丰富
  • 原理:slice存储了一个长度和一个容量,以及一个指向数组的指针,容量不够会扩容成一个新的切片
  • 切片操作不支持负数索引
    // 创建切片
    s := make([]string, 3) // 指定长度初始化
    // 追加元素
    s = append(s, "d","e") // append需要赋值回去
    // 切片操作-半开半闭区间
    fmt.Println(s[2:5])    // 取出s[2]-s[4]  
    fmt.Println(s[:5])     // 取出s[0]-s[4]
    fmt.Println(s[2:])     // 取出s[2]-s[4]
    // 遍历
    for i, v := range s {
        fmt.Println("index:", i, "value:", v)
    }
  • 切片扩容策略:
    如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
    注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的

map

  • 实际使用最频繁用到的数据结构
  • 完全无序, 随机顺序, 与插入顺序和字母顺序都无关 加一个ok用来获取到底由于key存在 map完全无序
    // 创建并初始化map
    m := make(map[string]int){"one":1} // key:string, value:int
    var m1 = map[string]int 
    // 添加键值对
    m["two"] = 2
    // 删除键值对
    delete(m, "two")
    // 访问
    r, ok := m["one"]   // 存在访问key则r为value,ok为true
                        // 不存在则r为0,ok为false
    // 可以直接打印
    fmt.Println(m)
    // 遍历
    for k, v := range m {
        fmt.Println("key:", k, "value:", v)
    }

range

  • 可以用来快速遍历slice或者map
  • 返回两个值,索引和对于位置值,不需要可以用下划线忽略

函数

  • 支持返回多值,常见返回数值和错误信息
  • 变量类型后置
    // 无返回值
    func add0(n int){
        n += 2
    }
    // 1个返回值
    func add(a, b int) int { // --> a int, b int
        return a+b
    }
    // 2个返回值
    func add1(a, b int) (c int, of bool) {
        c = a+b
        of = false
        return c, of
    }
    

指针:

  • 主要用途,函数参数的指针传递(调用时需取地址), 避免大结构体拷贝开销
    // 声明
    func add(n *int){
        *n += 2
    }
    // 调用
    n := 5
    add(&n)

结构体:

结构体是带类型的字段的集合 实现结构体方法

    // 结构体声明
    type user struct {
        name     string 
        password string
    }
    // 结构体方法实现
    func (u *user) checkPassword(password string) bool{
        return u.password == password 
    }
    // 初始化结构体
    a := user{name: "wang", password: "1024"}    // 方法1
    b := user{"wang", "1024"}                    // 方法2
    c := user{name: "wang"}; c.password="1024"   // 方法3
    var d user; d.name="wang"; d.password="1024" // 方法4
    // 结构体方法调用
    fmt.Println(a.checkPassword("202")) 

错误处理

  • 在Go语言中符号习惯的做法是使用一个单独的返回值来传递错误信息,在函数里,若函数返回类型有一个error,就代表这个函数会返回错误
    // 函数实现
    
  • Go使用简单的if else来处理错误
    // 形式1
    u, err := fundUser([]user{{"wang","1024"}}, "wang")
    if err != nil {    // 错误不为空值则打印并提前返回主函数
        fmt.Println(err)
        return
    }
    fmt.Println(u.name)// 没有错误, 正常流程
    // 形式2
    if u, err := fundUser([]user{{"wang","1024"}}, "wang"); err!=nil{
        fmt.Println(err)
        return
    }else{
        fmt.Println(u.name)
    }
 

字符串操作

标准库strings包里有很多常用的字符串工具函数 1674733673(1).png

JSON 处理:

对于已有结构体, 将每个字段的第一个字母改为大写,则该结构体可用json.marshaler序列化成一个JSON字符串, 序列化后的字符串也能用json.unmarshaler反序列到一个空的结构体变量里. 1674734359(1).png

时间处理:

   // 获取当前时间
   now := time.Now()
   // 构造时间
   t := time.Date(2022,3,27,1,25,36,0,time.UTC)
   // 获取时间的年份月份信息
   fmt.Println(t.Year(), t.Month(), t.Hour(), t.Minute())
   // 时间减法
   diff := now.sub(t)
   // 获取时间戳
   fmt.Println(now.Unix()) 

数字解析

数字和字符串之间的转换,在strconv包下 1674734998(1).png

进程信息

  • 包"os" 和 包"os/exec"
  • os.Args 获得进程在执行的时候的一些命令行参数
  • os.Getenv("PATH") 获取环境变量
    os.Setenv("AA", " BB") 设置环境变量
  • exec.Command().快速启动子进程并且获取其输入输出

输入输出流

参考文献

[1] 图解Golang垃圾回收机制! - 知乎 (zhihu.com)
[2] (28条消息) Golang常见面试题及解答_golang 面试_西木Qi的博客-CSDN博客 [3] golang的垃圾回收详解_golang垃圾回收_skyman满天星的博客-CSDN博客 [4] Go内存管理及性能观测工具_go 内存分析工具_黄豆酱的博客-CSDN博客