Go 语言详解

Golang 和 Java 都是流行的编程语言,但它们在语言特性、性能和生态系统等方面存在一些不同点。下面是它们的比较:

  1. 语言特性

Golang 是一门相对新兴的语言,它的设计主要借鉴了 C 语言的风格,使用静态类型和垃圾回收机制,最大特点是支持并发编程。而 Java 曾经非常流行,是一门支持面向对象编程的语言,通过虚拟机实现跨平台开发。Java 也支持并发编程,但在语言层面对并发支持不如 Golang。

  1. 性能

Golang 与 Java 在性能方面有一定的差距。Golang 的性能优势在于它具有轻量级的协程和高效的垃圾收集机制,而 Java 在垃圾回收方面并不优秀,在高并发场景下可能出现大量 CPU 消耗和 GC 压力。

  1. 生态系统和支持

Java 的生态系统非常成熟,有丰富的类库和开源框架,如 Spring、Hibernate 等,以及许多开发工具,比如 Eclipse、IntelliJ IDEA 等。而 Golang 的生态系统相对较新,虽然也有一些类库和框架,如 Gin、Echo 等,但相比于 Java 来说还是不如。

总的来说,Golang 和 Java 是两门不同的编程语言,各有各的特点。如果需要高效实现并发编程,可以考虑使用 Golang;如果需要一个成熟的生态系统和丰富的类库支持,可以选择 Java。在实际使用中,可以根据具体场景选择合适的语言和工具,以达到最优的效果。

一、基本介绍

Golang(又称 Go)是一种开源的编程语言,由 Google 公司开发。Golang 语言的设计目标是提高程序员的开发效率和系统性能,融合了 C 和 Java 等语言的优点,包括静态类型、高效编译、垃圾回收、安全性等特点,同时也支持并发编程。

目前,Google, Facebook,Docker,Netflix ,字节跳动等公司使用 golang 进行开发。

二、 Golang 语言的优点

2.1 高效性

Golang 的编译速度非常快,生成的可执行文件也非常小,且支持并发编程,可以极大的提高程序性能。

2.2 并发能力强

Golang 提供了原生的协程 (goroutine) 和通道(channel)管理机制,支持高效的并发编程

2.3 代码简洁

Golang 的语法简洁,清晰易读,可以极大的提高开发效率,减少代码复杂度。

2.4 内存安全

Golang 的垃圾回收机制可以自动回收内存,同时还提供了多种内存安全和数据安全的机制,可以避免很多常见的安全问题。

2.5 组件丰富

Golang 具有非常丰富的标准库和开发根据,可以降低开发的复杂度。

三、调度的实现

Golang 通过协程 (goroutine)和调度器(scheduler)来完成调度的。
Golang 可以很方便的启动一个协程
例如

go func() {
    // some logic
}()

而这些协程的运转主要靠调度器实现,这也是 Golang 语言需要注意的关键。
下面对调度器的实现细节进行具体分析

3.1 调度器实现的基本概念

Golang 的调度器是一种 M:N 的调度器,其中 M 代表内核线程的数量,N 代表协程的数量。和普通调度器不同, golang 的调度器会根据应用程序的变化自动调整 M 和 N 的值。

3.2 G-M 模型

Golang 调度器的实现采用了 G-M 模型。G 代表 goroutine 即一个协程,他执行在 M (内核线程)绑定的 P (processer )对象上。调度器有一个全局的运行队列,存储所有准备运行的协程。协程 G 被创建就会被放入运行队列。当协程 G 执行时,他会被调度器从运行队列取出,被 M 对应的 P 执行。直到执行时间超过预定的时间片,或是协程主动调用了 runtime.Gosched 方法后,他会被调度器放回执行队列,换一个协程来运行。

3.3 自适应线程

Golang 的调度器采用了自适应线程机制。当一个 M 无线程可运行时,调度器会创建一个线程绑定在 M 上。当发现一个线程处于空闲状态时,调度器会将对应的 M 和线程都销毁,以便资源得到回收。

3.4 抢占式调度

Golang 的调度器使用了抢占式调度的机制,协程的执行可能会被更高优先级的协程中断。Golang使用内置的 runtime.Goexit 函数或通道来进行阻塞,实现协程的切换。

3.5 堆栈管理

调度器在每个协程的栈上设置了边界,超过时会自动触发扩容操作。协程的栈也会被标记,方便之后的回收。

四、 回收机制

Golang 的回收机制主要基于标记-清除 算法。

4.1 go1.3 —— 标记清除算法

流程:

  1. 标记:GC 从根对象开始,遍历所有对象堆,将可达的对象标为存活的对象。Golang 设置有指针结构,在内存中维护了指向堆对象的指针,遍历效率较高。
  2. 清除:GC 扫描整个堆,将未被标记的对象标为垃圾。这些垃圾对象会被释放。

缺点:

会导致 STW(程序暂停)。即垃圾回收时,程序会出现卡顿,严重影响性能。

4.2 go1.5 —— 三色标记法

设定了 白色,灰色,黑色标记表

流程:

  1. 新创建的对象标记为白色
  2. 从根节点出发,将遍历到的对象从白色集合移到灰色集合
  3. 遍历灰色集合,将灰色对象应用的白色对象放入灰色集合。将原灰色集合的对象放到黑色集合,
  4. 重复,知道灰色表中无对象。
  5. 回收所有剩下的白色对象。

比标记清除算法的优化

标记流程不需要 STW

存在的问题

当白色对象被黑色对象引用,灰色对象对白色对象的引用消失时。白色对象会被误回收。

解决方案

强三色不变式:不允许黑色对象引用白色对象
实现:插入写屏障(A引用 B 时,将 B 标为灰色)
缺点:限制的了堆对象,限制不了栈对象。因为栈数量较多,每个协程都有,开屏障会让开销过大,所以不能插入

弱三色不变式:只有当该白色对象同时是灰色对象的引用下游时,它才能被黑色对象引用。
实现:删除写屏障(被删除的对象,假如自身为白色,会被标记为灰色)
缺点:回收精度下降,这个对象不存在引用后,还能存活一轮 GC,直到下一轮才会被回收。影响内存。

4.3 go1.8 —— 三色标记 + 混合写屏障

混合写屏障

GC 开始时会将栈上的可达对象标记为黑色。
GC 期间,栈上创建的对象和引用白色对象都会被标为黑色。
堆上被删除或添加的白色对象会被标为灰色

4.4 Golang 的 GC 触发

  1. 定量触发:默认配置是堆内存到上次 GC 的内存两倍时触发。默认值是 100%。即增长 100 %的堆内存才会触发 GC。
  2. 定时触发:由 runtime.forcegcperiod 变量控制。默认两分钟。
  3. 手动触发:调用 runtime.GC 函数触发
  4. 空间不足时触发:内存无空闲空间时,创建 32KB 以下可能触发,以上必然触发。

五、 Channel 的实现

Channel 本质上是一个带缓冲的队列,并有首尾指针。读写时,首尾指针的位置会被更改。
缓冲区慢时,写入会被阻塞,缓冲区空时,读取会被阻塞
写入只能串行进行。
goroutine 调用时,当有数据或缓冲区可用时。调度器会按照规则调用 goroutine。

数据完整性保证:数据块是原子性传输。

六、 Golang 性能优化

6.1 减少内存分配,避免垃圾回收

使用内存池,用标准库提供的 sync.pool 类型,来创建一个内存池进行内存复用。
避免拼接字符串,这个会导致内存额外分配。使用 bytes.Buffer 来优化
使用引用类型。传递时就只需要传递指针,减少内存分配。如 slice,map,channel

6.2 利用并发优势

Golang 天生支持协程和通道,可以很方便的并发变成,将需要并发的任务拆分成多个协程处理,来提高效率

6.3 函数调用优化

采用行内函数,减少不必要的类型转换,避免多次分配内存的方式优化性能。

6.4 优化数据结构

通过使用合适的数据结构,来减少内存分配,遍历次数,来提高程序性能。

七、 一些题目

7.1 如何使用两个 goroutine 交替打出 1,2

import (
	"fmt"
    "sync"  
)
 
func main() {
  letter, number := make(chan bool), make(chan bool)
  wait := sync.WaitGroup{}
  go func() {
    i := 1
    for {
      select {
      case <-number:
        fmt.Println(i)

        letter <- true
        break
      default:
        break
      }
    }
  }()
  
  wait.Add(1)
  go func(wait *sync.WaitGroup) {

    i := 2
    for {
      select {
      case <-letter:
        fmt.Println(i)
        number <- true
        break
      default:
        break
      }
    }
  }(&wait)

  number <- true
  wait.Wait()
}

7.2 make 和 new 的区别

make 用于创建数组,切片和 map ,返回的是类型的引用。且会对创建的数据类型进行初始化,包括长度和容量。
new 用于创建结构体对象,并分配内存,返回的是分配类型的指针。不会初始化内存。

func main() {
    // 使用 make 分配内存
    s := make([]int, 3, 5)
    fmt.Println(s) // [0 0 0]

    // 使用 new 分配内存
    p := new(int)
    fmt.Println(*p) // 0
}

7.3 单个 package 中, init 函数与常量,全局变量的执行顺序

常量 -》 全局变量 -》 init 函数

7.4 slice 的扩容逻辑

小于 1024 时,翻倍扩容
大于 1024 时,每次扩 四分之一,直到符合需要。
扩容时,先分配足够大的内存,再复制数据过去。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值