Go基础知识

Go基础知识超线程技术和 CPU 多核化的普及为并行计算提供了技术支持和编程需求 程序的并发度有了极大的提升

大家好,欢迎来到IT知识分享网。

目标

  • 简单介绍一下 GO 语言的诞生背景,使用场景,目前使用方案
  • 简单介绍一下 GO的使用,GO的基础语法,简单过一下一些GO的语言例子
  • 着重介绍一下GO的特性,为什么大家都使用GO语言,GO的内存结构、为什么都说GO快
  • 论述清楚为什么大家都在开始使用GO

1 Go语言概述

1. 1Go语言的诞生背景

  • 静态强类型:静态强类型语言是指在编译阶段就确定每个变量的数据类型的编程语言,并且这个数据类型在后续的代码中是不允许改变的。
  • 编译型:代码在执行前需要经过一个转换过程,也就是编译。编译器会将源代码(人类可读的高级指令)转换成目标代码(机器可执行的低级指令),并生成一个可执行文件。这个可执行文件可以在没有源代码的情况下运行。Java是一种既编译又解释的语言。
    在这里插入图片描述

1.2 Go语言的使用场景

Go在诞生之初,就以出色的性能和高效的并发处理能力,被广泛应用在以下的场景:

  1. 服务器端开发:Go语言非常适合用于构建大型、高性能的服务器端应用程序。它的并发处理能力和高效的I/O操作使其成为处理高并发请求的理想选择。越来越多的公司开始拥抱Go语言。
  2. 分布式系统:Go语言具有对并发的天然支持,其内置的goroutine和channel使得并发编程变得简单而高效。这使得Go语言成为构建分布式系统的理想选择,可以方便地实现并发的任务分发和数据同步。
  3. 云计算:Go语言具有快速编译、高效执行和低资源消耗等特点,非常适合用于构建云计算平台、容器编排工具、云存储等。许多知名的云计算项目,如Docker、Kubernetes等,都是使用Go语言开发的。
  4. 数据库和存储系统、系统编程、DevOps工具等

2 Go语言基础

2.1 Go语言的基础语法

以一个经典的Hello World示例:

// 指定包名 package main // 导包 import "fmt" // 函数定义 函数名(参数) 返回值 func main() { 
    // 使用标准库进行打印 fmt.Println("Hello, World!") // 结束分句不需要 ; } 

与Java相比有几点差异:

  1. 不需要分号:在Go语言中,一行代码的结束不需要分号,这与Java和许多其他C系列语言不同。
  2. 强制大括号:在Java中,如果if,for等语句的主体只有一行代码,可以省略大括号,但在Go语言 > 中,无论主体部分有多少行代码,都必须使用大括号。
  3. 缩进风格:Go语言采用的是Tab键进行缩进,而Java则是使用四个空格。
  4. 错误处理:Go语言没有异常处理,而是通过多值返回和错误接口进行错误处理,这与Java的try-> catch-finally异常处理方式不同。
  5. 变量声明:Go语言的变量声明方式也与Java不同,Go语言使用 var 关键字声明变量,同时Go语言还支持 “:=” 形式的短变量声明和初始化。
  6. 公有和私有:Go语言中,首字母大写的函数、变量是公有的,首字母小写的是私有的,而在Java中,公有和私有由关键字public和private表示。
  7. 类型声明:在Go语言中,类型声明放在变量名之后,而在Java中,类型声明放在变量名之前。
  8. 资源管理:Go语言使用defer关键字进行资源管理,使得资源的释放操作可以紧跟在资源的获取操作之后,而不需要去关心何时进行资源的释放。

2.2 Go语言的基本数据类型

// 布尔类型 // var : go关键字,用来声明变量,类型可加可不加,会进行类型推导 // 若不需要指定变量类型,也可以指定为:b := true,表示声明变量且赋值 var b bool = true cBool := true // 数字类型 var i int = 10 var f float32 = 12.34 // 字符串类型 var s string = "Hello Go" var arr1 [5]int // 声明了一个int类型的数组 arr2 := [3]int{ 
   1,2,3} // 声明了一个长度为3的int数组 arr3 := [...]int{ 
   2, 4, 6, 8, 10} // 声明了一个长度为5的int数组,元素是2, 4, 6, 8, 10 // 切片会自动扩容 var mySlice []int //声明一个int切片 mySlice = []int{ 
   2, 3, 5, 7, 11, 13} // 创建包含6个元素的切片 mySlice = []int{ 
   2, 3, 5, 7, 11, 13}[1:4] // 创建包含原切片元素1-4的切片 // map类型 var m map[string]int m["key1"] = 1 

Go语言提供了一种简单、直接、灵活的方式来处理数据类型,可以根据实际需要来构造出复杂的数据类型。

2.3 Go语言的控制结构

if else 结构 package main import"fmt" func main() { 
    var a int = 100 if a < 20 { 
    fmt.Printf("a 小于 20\n" ) } else { 
    fmt.Printf("a 不小于 20\n" ) } } 
循环结构只有for循环,没有while关键字: // 标准的for循环语句,初始化变量、循环判断条件,后处理,这三个部分都可以不指定 for i := 0; i < 10; i++ { 
    fmt.Println(i) } // 类似while循环的使用 i := 0 for i < 10 { 
    fmt.Println(i) i++ } // 无限循环 for { 
    fmt.Println("This loop will run forever.") } 

2.4 Go语言的函数

package main import "fmt" // 定义函数 函数名(参数A, 参数B) 返回值 func add(x int, y int) int { 
    return x + y } func main() { 
    // 调用函数 fmt.Println(add(42, 13)) } 

2.5 Go语言的错误处理方式

package main import ( "fmt" "errors" ) // 我们定义了一个f1函数,该函数在参数等于42时返回一个错误,否则返回参数加3的结果。 func f1(arg int) (int, error) { 
    if arg == 42 { 
    return -1, errors.New("can't work with 42") } return arg + 3, nil } func main() { 
    // 对f1函数的返回值进行了错误处理,如果返回错误,打印"f1 failed:“,否则打印"f1 worked:”。 for _, i := range []int{ 
   7, 42} { 
    if r, e := f1(i); e != nil { 
    fmt.Println("f1 failed:", e) } else { 
    fmt.Println("f1 worked:", r) } } } 

2.7 Go语言的面向对象编程

2.7.1 结构体(Structs)
type Person struct { 
    Name string Age int } func main() { 
    p := Person{ 
   Name: "Alice", Age: 20} fmt.Println(p.Name) // 输出: Alice } 
2.7.2 方法(Methods)

在Go语言中,可以给任意类型(包括结构体)定义方法。方法的定义形式和函数类似,只是在函数名前多了一个接收者参数,接收者可以是任意类型的变量。这样我们就可以在这个变量上调用这个方法。

type Rectangle struct { 
    Width, Height float64 } // 为Rectangle定义Area方法 func(r Rectangle) Area() float64 { 
    return r.Width * r.Height } func main() { 
    r := Rectangle{ 
   Width: 10, Height: 5} fmt.Println(r.Area()) // 输出: 50 } 
2.7.3 接口(Interfaces)
type Shape interface { 
    Area() float64 } type Shape2 interface { 
    Area() float64 } type Circle struct { 
    Radius float64 } // Circle实现Shape接口 func (c Circle) Area() float64 { 
    return math.Pi * c.Radius * c.Radius } func main() { 
    c := Circle{ 
   Radius: 5} var s Shape2 = c fmt.Println(s.Area()) // 输出: 78.483 } 
2.7.4 继承与组合
type Person struct { 
    Name string Age int } type Student struct { 
    Person // 嵌入Person School string } func main() { 
    s := Student{ 
   Person: Person{ 
   Name: "Alice", Age: 20}, School: "MIT"} fmt.Println(s.Name) // 输出: Alice } 
2.7.5 多态
type Walker interface { 
    Walk() } 

然后我们定义了一个Duck类型,并为其实现了Walk()方法:

type Duck struct { 
   } func(d Duck) Walk() { 
    fmt.Println("Duck walks") } 

这样,Duck就被认为实现了Walker接口,即使我们并没有显式声明Duck实现了Walker接口。然后我们就可以把Duck类型的对象赋值给Walker类型的变量,通过这个变量调用Walk()方法:

var w Walker = Duck{ 
   } w.Walk() // 输出:Duck walks 

如果一个类型需要实现多个接口,并且这些接口中有相同的方法,那么这个类型只能提供一个实现,这个实现会被所有调用这个方法的接口共享。

2.8 Go语言的并发模型和Channel

2.8.1 Go语言的Goroutine
go func() { 
    fmt.Println("This is a goroutine") }() fmt.Println("This is main goroutine") 

代码中,创建了一个Goroutine并在其中打印消息,然后在主Goroutine中打印消息。由于Goroutine的调度是由Go运行时进行的,所以两个打印语句的执行顺序是不确定的。

2.8.2 Go语言的Channel

2.8.3.1 Channel的基本概念和作用

Channel是Go语言中的一个核心类型,可以把它看成一个管道,可以通过它发送类型化的数据,在不同Goroutine之间进行通信,Channel是支持并发的。示例代码如下:

ch := make(chan int) go func() { 
    ch <- 1 // 向ch中发送数据 }() fmt.Println(<-ch) // 从ch中接收数据 
ch := make(chanint) // 创建一个整型的Channel 关闭Channel的语法如下: close(ch) // 关闭Channel 
ch <- 1// 向ch发送一个整数x := <-ch // 从ch接收一个整数并赋值给x 

如果Channel已经关闭,接收操作将立即完成,接收到的值为元素类型的零值。

2.6 Go语言编程实战

2.6.1 Go语言的Channel

生产者消费者。

3. Go语言特性及优势

为什么说Go提高开发效率?

  1. 语法简洁:Go语言的语法非常简洁,只有极少数的关键词,没有复杂的类继承,使得写代码变得更直接、更容易理解。这种简洁性使得学习曲线陡峭,新手更容易上手
  2. 编译速度、启动速度快:Go语言编译的是静态二进制文件,运行时不需要JVM(Java虚拟机)的启动和类加载过程,所以启动速度通常更快。启动速度快意味着在做开发时,调试的速度更快,在Docker等方式部署应用时能够更快run起来。
  3. 跨平台:Go语言编译后的执行文件可以直接在目标操作系统上运行,无需其他依赖,这使得部署应用变得非常简单,提高了开发效率。
  4. 运行占用内存小:Go语言使用静态编译,直接编译成机器语言运行,不需要像Java那样需要JVM来运行,因此在内存占用上会较小。
    在这里插入图片描述

3.1 Go语言的并发模型

3.1.1 GORoutine(协程)
  • 线程分为内核线程和用户态线程,协程是一种用户态线程。
    用户态线程跟内核态线程主要的2点区别:
  1. 管理方式:内核线程由操作系统内核管理和调度,而用户态线程由用户程序自己管理和调度。内核线程可以直接使用操作系统提供的调度算法,而用户态线程需要在用户程序中实现调度算法。
  2. 上下文切换:内核线程的上下文切换涉及到用户态和内核态之间的切换(切换时要校验指令等操作),所以开销比较大。用户态线程的上下文切换只发生在用户态,开销较小。
    Go Routine是Go语言中的一种轻量级用户态线程实现,也是Go语言中并发编程的核心。Go Routine与普通的线程或进程不同,它由Go运行时(Go Runtime)管理。

Go Routine的优点:

  1. 创建和销毁的成本更低:Go Routine的创建和销毁的成本远低于线程和进程,每个GoRoutine的栈初始只有2KB,对比普通的线程栈大小在MB级别。这让我们可以大量地创建Go Routine来处理任务,而不必担心系统资源被耗尽。
  2. 切换的成本更低:Go Runtime可以自动进行Go Routine之间的切换,与线程上下文切换相比,其成本更低。
  3. 更简单的同步机制:Go语言内建了Channel(管道)和Select机制,使得Go Routine之间的同步和通信更为简单高效。
    使用Go语言的go关键字,可以非常简单地启动一个Go Routine来并发执行任务,如:
    go doSomething()
    在这行代码中,doSomething函数将会在一个新的Go Routine中并发执行,而不会阻塞当前的程序执行流程。


3.1.2 Go的并发调度模型
  • G: Go Routine,代表一个待执行的任务。
  • P: Processor,处理器,代表Go语言的调度上下文环境,每个P都有一个本地的Go Routine队列,并且每个P在同一时间只能被一个线程(M)所拥有。
  • M: Machine,代表一个操作系统的线程。
    Go语言的调度器采用了M:N的调度模型,也就是说,M个Go Routine会被N个操作系统线程所调度和执行。
    站在CPU的角度,一个线程可以持久的占用住CPU,Go Routine并发的使用一个线程资源去执行任务,减少线程调度带来的资源消耗。
    在这里插入图片描述


在Go语言的调度模型中:

  • P的数量由GOMAXPROCS环境变量决定,这个变量默认值是机器的CPU核数。P的数量决定了可以同时运行的Go Routine的数量。
  • M的数量没有上限,当所有的P都被占用,且有新的Go Routine需要运行时,Go运行时会创建一个新的M来运行这个Go Routine。
  • 当M在执行G的过程中,如果G发生了阻塞(如等待IO、调用系统调用等),那么M会将自己和G解绑,然后去运行其他的G。当G解除阻塞时,会再次被某个M所绑定并执行。
    • 如果当前的P的本地队列已满,那么运行的G会尝试将新的Goroutine放入全局队列或者其他P的本地队列,然后唤醒一个空闲的P和M组合来执行新的Goroutine。
      下面是具体的工作流程:
  1. 当一个Go程序启动时,会首先创建一个主G和一个主M,然后创建由GOMAXPROCS指定数量的P。主M会获取一个P,然后开始执行主G。
    在这里插入图片描述
  2. 在程序执行过程中,可能会创建新的G。新创建的G会首先被放到当前P的本地G队列中。如果本地G队列已满,就会被放到全局G队列中。
  3. 当一个M的当前G阻塞时(例如等待IO、系统调用、channel操作等),M会将自己和G解绑,然后去P的本地G队列中获取一个新的G来执行。如果本地G队列为空,M会尝试去全局G队列中获取G,或者从其他P的本地G队列中偷取G。如果都获取不到G,M就会阻塞,直到有新的G可执行。
  4. 当一个阻塞的G变为可执行状态时(例如等待的IO完成、系统调用返回、channel收到数据等),Go运行时会创建或者唤醒一个M,然后将G和M绑定,然后M会获取一个P,然后开始执行G。
  5. Go程序退出时,所有的M和P都会被销毁,所有的G都会被停止。
    Go语言的调度器在设计中采取了一些策略,以实现高效的并发性能:
  6. M:N 调度: Go语言的调度器使用了M:N的调度模型,这意味着M个Go协程(Go Routines)会被N个系统级线程(OS threads)调度和执行。这样的设计使得Go语言能够创建大量的协程而不会消耗大量的系统资源。
  7. 工作窃取(Work Stealing):当某个处理器(P)的本地Go协程队列为空时,它会尝试从全局Go协程队列或其他处理器的本地Go协程队列中窃取一半的协程。这种策略使得各个处理器之间的工作负载更加平衡,避免了某个处理器闲置而其他处理器的任务过多的情况。
    在这里插入图片描述
  8. 抢占式调度(Preemptive Scheduling):从Go 1.14版本开始,Go语言的调度器实现了真正的抢占式调度。这种策略能够防止长时间运行的协程阻塞其他协程的执行,保证了系统的响应性。这是通过插入一种称为”抢占请求”的机制实现的。当调度器决定需要抢占一个正在运行的协程时,它会向这个协程的栈插入一个抢占请求。然后,当这个协程下一次执行函数调用时,它会检查这个抢占请求,并主动让出CPU。
    在这里插入图片描述

3.2 Go语言的内存结构

3.2.1 TCMalloc

在这里插入图片描述

  • Page
    操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。
  • Span
    一组连续的Page被称为Span,比如可以有4个页大小的Span,也可以有8个页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
  • ThreadCache
    每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
  • CentralCache
    是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的。
  • PageHeap
    PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap。
  • TCMalloc对象分配
    小对象直接从ThreadCache分配,若ThreadCache不够则从CentralCache中获取内存,CentralCache内存不够时会再从PageHeap获取内存,大对象在PageHeap中选择合适的页组成span用于存储数据。
3.2.2 Go的内存管理

在这里插入图片描述

  • mcache
    mcache与TCMalloc中的ThreadCache类似,mcache保存的是各种大小的Span,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问。但mcache与ThreadCache也有不同点,TCMalloc中是每个线程1个ThreadCache,Go中是每个P拥有1个mcach,因为在Go程序中,当前最多有GOMAXPROCS个线程在运行,所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问。
  • mcentral
    mcentral与TCMalloc中的CentralCache类似,是所有线程共享的缓存,需要加锁访问,它按Span class对Span分类,串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。但mcentral与CentralCache也有不同点,CentralCache是每个级别的Span有1个链表,mcache是每个级别的Span有2个链表。
    为什么span要分级别?
    在这里插入图片描述


  • mheap
    mheap与TCMalloc中的PageHeap类似,它是堆内存的抽象,把从OS(系统)申请出的内存页组织成Span,并保存起来。当mcentral的Span不够用时会向mheap申请,mheap的Span不够用时会向OS申请,向OS的内存申请是按页来的,然后把申请来的内存页生成Span组织起来,同样也是需要加锁访问的。但mheap与PageHeap也有不同点:mheap把Span组织成了树结构,而不是链表,并且还是2棵树,然后把Span分配到heapArena进行管理,它包含地址映射和span是否包含指针等位图,这样做的主要原因是为了更高效的利用内存:分配、回收和再利用。
    在Go的内存管理模型中,scav和free span都使用了treap作为其主要的数据结构。treap是一种特殊的二叉搜索树,它结合了二叉搜索树和堆的特性。
    在treap中,每个节点都有一个键(key)和一个优先级(priority)。树按照键来排序,但是节点的布局又遵循堆的性质,即每个节点的优先级都高于或等于其子节点的优先级。这样,treap既能像二叉搜索树一样高效地进行查找操作,又能像堆一样高效地进行插入和删除操作。


基于以上数据结构,go当中一个对象的创建流程如下:

  1. 当一个 goroutine 需要创建一个对象时,首先会尝试从它自己的 mcache 中分配内存。mcache 是每个 goroutine 独享的小型内存缓冲区,用于存储一些小的、经常使用的对象。
  2. 如果 mcache 中的内存不足,那么 goroutine 会尝试从 mcentral 中获取内存。此操作过程需要进行加锁操作。
  3. 如果 mcentral 中的内存也不足,那么 mcentral 会从 mheap 中申请一大块内存。mheap 是一个全局的大型内存池,是从操作系统直接申请到的内存。此操作过程需要进行加锁操作。
  4. 如果 mheap 中的内存也不足,那么 Go 会向操作系统请求更多的内存。如果操作系统无法提供更多的内存,那么 Go 会抛出内存不足(out of memory)的错误。
  5. 一旦获取到足够的内存,Go 就会在这块内存上创建对象,并返回这个对象的引用。
    对于大对象的创建,Go 语言的处理方式与小对象有所不同。在 Go 语言中,被定义为大对象的具体大小阈值可能会根据不同的实现和平台有所不同。但一般来说,如果一个对象的大小超过了一个阈值(比如32KB),那么它就会被视为一个大对象。
    对于大对象,Go 语言通常会直接在堆(heap)上进行分配。大对象不会经过 mcache 或 mcentral,而是直接从 mheap 中申请内存。这是因为大对象由于其大小,不适合放在 mcache 或 mcentral 中,否则会占用过多的空间和内存碎片。

4 总结

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/119525.html

(0)
上一篇 2025-11-04 20:26
下一篇 2025-11-04 20:45

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信