GO_高阶

interface 接口

  在 Go 中接口是一种抽象类型,是一组方法的集合,里面只声明方法,而没有任何数据成员。

  而在 Go 中实现一个接口也不需要显式的声明,只需要其他类型实现了接口中所有的方法,就是实现了这个接口。

  • 语法

    • type <interface_name> interface {
          <method_name>(<method_params>) [<return_type>...]
          ...
      }
  • 注意

    • Go 中接口声明的方法并不要求需要全部公开
    • 接口可以嵌套
    • 直接用接口类型作为变量时,如果方法都是值接收者,可以用值或指针。如果任何方法是指针接收者,则必须用指针。
    • 如果函数参数使用 interface{}可以接受任何类型的实参。同样,可以接收任何类型的值也可以赋值给 interface{}类型的变量
      • package main
        
        import "fmt"
        
        // PaymentMethod 接口定义了支付方法的基本操作
        type PayMethod interface {
            Account
            Pay(amount int) bool
        }
        
        type Account interface {
            GetBalance() int
        }
        
        // CreditCard 结构体实现 PaymentMethod 接口
        type CreditCard struct {
            balance int
            limit   int
        }
        
        func (c *CreditCard) Pay(amount int) bool {
            if c.balance+amount <= c.limit {
                c.balance += amount
                fmt.Printf("信用卡支付成功: %d\n", amount)
                return true
            }
            fmt.Println("信用卡支付失败: 超出额度")
            return false
        }
        
        func (c *CreditCard) GetBalance() int {
            return c.balance
        }
        
        // DebitCard 结构体实现 PaymentMethod 接口
        type DebitCard struct {
            balance int
        }
        
        func (d *DebitCard) Pay(amount int) bool {
            if d.balance >= amount {
                d.balance -= amount
                fmt.Printf("借记卡支付成功: %d\n", amount)
                return true
            }
            fmt.Println("借记卡支付失败: 余额不足")
            return false
        }
        
        func (d *DebitCard) GetBalance() int {
            return d.balance
        }
        
        // 使用 PaymentMethod 接口的函数
        func purchaseItem(p PayMethod, price int) {
            if p.Pay(price) {
                fmt.Printf("购买成功,剩余余额: %d\n", p.GetBalance())
            } else {
                fmt.Println("购买失败")
            }
        }
        
        func main() {
            creditCard := &CreditCard{balance: 0, limit: 1000}
            debitCard := &DebitCard{balance: 500}
        
            fmt.Println("使用信用卡购买:")
            purchaseItem(creditCard, 800)
        
            fmt.Println("\n使用借记卡购买:")
            purchaseItem(debitCard, 300)
        
            fmt.Println("\n再次使用借记卡购买:")
            purchaseItem(debitCard, 300)
        }

并发-goroutine 与 channel

  go 支持并发的方式,就是通过 goroutine 和 channel 提供的简洁且高效的方式实现的。

  • goroutine

  goroutine 是轻量线程,创建一个 goroutine 所需的资源开销很小,所以可以创建非常多的 goroutine 来并发工作。

  它们是由 Go 运行时调度的。调度过程就是 Go 运行时把 goroutine 任务分配给 CPU 执行的过程。但是 goroutine 不是通常理解的线程,线程是操作系统调度的。

  在 Go 中,想让某个任务并发或者异步执行,只需把任务封装为一个函数或闭包,交给 goroutine 执行即可。

    • 声明

      • 声明方式 1,把方法或函数交给 goroutine 执行:
        go <method_name>(<method_params>...)
        
        声明方式
        2,把闭包交给 goroutine 执行: go func(<method_params>...){ <statement_or_expression> ... }(<params>...)
    • 示例

      • package main
        
        import (
            "fmt"
            "time"
        )
        
        func say(s string) {
            for i := 0; i < 5; i++ {
                time.Sleep(100 * time.Millisecond)
                fmt.Println(s)
            }
        }
        
        func main() {
            go func() {
                fmt.Println("run goroutine in closure")
            }()
            go func(string) {
            }("gorouine: closure params")
            go say("in goroutine: world")
            say("hello")
        }
  • 同步原语

      提到并发编程、多线程编程时,我们往往都离不开『锁』这一概念,Go 语言也为开发者提供这一功能,锁的主要作用就是保证多个线程或者 Goroutine 在访问同一片内存时不会出现混乱的问题,锁其实是一种并发编程中的同步原语(Synchronization Primitives)。

   在这一节中我们就会介绍 Go 语言中常见的同步原语 MutexRWMutexWaitGroupOnceCond 以及扩展原语 ErrGroupSemaphoreSingleFlight 的实现原理,同时也会涉及互斥锁、信号量等并发编程中的常见概念。

    • sync.Mutex - 互斥锁

一份互斥锁对一个资源加锁,只能同时被一个 goroutine 锁定,其他 goroutine 阻塞等待资源释放。

Go 语言中的互斥锁在 sync 包中,它由两个字段 statesema 组成,state 表示当前互斥锁的状态,而 sema 真正用于控制锁状态的信号量,这两个加起来只占 8 个字节空间的结构体就表示了 Go 语言中的互斥锁。

      • 锁结构

        • type Mutex struct {
              state int32
              sema  uint32
          }
        • state:互斥锁的当前状态
          mutexLocked — 表示互斥锁的锁定状态;
          mutexWoken — 表示从正常模式被从唤醒;
          mutexStarving — 当前的互斥锁进入饥饿状态;
          waitersCount — 当前互斥锁上等待的 Goroutine 个数;

        • sema:控制锁状态
          正常模式 - 锁的等待者会按照先进先出的顺序获取资源。
          刚被唤起的 goroutine 与新建的 goroutine 竞争资源的时,大概率竞争失败而无法获取锁资源,所以若 goroutine 超过 1ms 没有获取到锁,互斥锁会自动被切换成饥饿模式,防止 goroutine 没有资源。
          饥饿模式 - 互斥锁会直接将资源交给等待队列最前的 goroutine,新创建的 goroutine 会被至于等待最尾端。目的是为了确保互斥锁的公平性。
          若一个 goroutine 获得了互斥锁,并且它在队列尾端,或者等待的时间少于 1ms ,则互斥锁会自动切换成正常模式
          饥饿模式可以有效的避免 goroutine 陷入由于等待无法获取锁的高危延时。

 

      • 示例

        • sum:= 0
          mutex := sync.Mutex
          
          func add(i int) {
            mutex.Lock()
              defer mutex.Unlock() //采用defer 语句释放锁。
              sum += i
          }
          
          // 被解锁保护的sum += i 代码片段又称为临界区。在同步原语设计中,临界区指的是一个访问共享资源的代码片段,而这些共享资源又有无法同时被多个协程访问的特征。当有协程进入临界区段时,其它协程必须等待,这样就保证了临界区的并发安全。
          
          func main() {
              for i := 0; i < 100; i ++ {
              go add(10)
              }
          
              for i := 1; i <10 ; i++ {
              go fmt.Println("和为:", getSum())
              }
          
              time.Sleep(2 * time.Second)
          }
          
          
          func getSum() int {
              mutex.Lock()
              defer mutex.Unlock()
              b := sum
              return b
          }

 

    • sync.RWMutex

 

  因为add 和 getSum函数使用的是同一个sync.Muetx,所以它们的操作是互斥的,也就是一个goroutine进行修改操作时,另一个goroutine读取操作会等待,直到修改操作完毕。

​   我们解决了goroutine同时读写的资源竞争问题,但是又遇到了另一个问题——性能。因为每次读写共享资源都要加锁,所以性能底下,如何解决呢?

  所以可以通过sync.RWMutex 来优化此段代码,提升性能。改用读写锁,来实现我们想要的结果

 

      • 语法

 

        • var mutex snyx.RWMutex
          
          func getSum() int {
              // 获取读写锁
              mutex.RLock()
              defer mutex.RUnlock()
              b := sum
              return b
          }
          
          // 这样性能会有很大提升,因为多个goroutine可以同时读取数据,不再相互等待。
    • sync.WaitGroup

  我们上述的示例中使用了time.Sleep函数, 是为了防止主函数main() 返回,一旦main函数范湖了, 程序也就退出了。 提示:一个函数或方法的返回(return) 也就意味着当前函数执行完毕。

​   如我们我们执行100个add协程和10个getSum协程,不知道什么时候执行完毕,所以设置了比较长的等待时间,但是存在一个问题,如果这110个协程很快就执行完毕,main函数应该提前返回,但是还要等待一会才能返回,会产生性能问题。但是如果等待时间截止时,协程没有执行完毕,程序会提前退出,导致有协程没有执行完,产生不可预知的结果。

​   如何解决这个问题呢? 有没有办法监听所有协程的执行,一旦全部执行完毕,程序马上退出,这样既可保证所有协程执行完毕,又可以及时退出节省时间,提升性能。Channel可以解决这个问题,不过很复杂。Go语言提供了更简洁的方法,它就是 sync.WaitGroup。

      • 语法

        • sync.WaitGroup使用比较简单,一共分为三步:

          • 声明一个sync.WaitGroup,然后通过Add方法设置计数器的值,需要跟踪多少个协程就设置多少

          • 在每个协程执行完毕时调用Done方法,让计数器减1,告诉sync.WaiGroup 该协程已经执行完毕

          • 最后调用Wait方法一直等待,直到计数器值为0,也就是所有跟踪的协程都执行完毕

 

  ​ 通过sync.WaitGroup可以很好地跟踪协程。在协程执行完毕后,整个函数才会执行完毕,时间不多不少,正好是协程执行的时间。

  sync.WaitGroup适合协调多个协程共同做一件事情的场景,比如下载一个文件,假设使用10个协程,每个协程写在1/10,只有10个协程都下载好了整个文件才下载好了。这就是我们经常听说的多线程下载,通过多个线程共同做一件事情,显著提高效率。小提示我们可以把Go语言的协程理解为平常说的线程,从用户体验上并无不可,但是从技术实现上,它们是不一样的。

 

      • 示例

        • func run() {
              var wg sync.WaitGroup
              wg.Add(110)
              
              for i := 0; i< 100; i ++ {
                  go func() {
                    // 计数器减1
                      defer wg.Done()
                      add(10)
                  }()
              }
              
              for i := 1; i < 10 ; i++ {
                  go func() {
                      defer wg.Done()  
                      fmt.Println("sum is", getSum())
                  }()
              }
              
              // 一直等待,直到计数器值为0
              wg.Wait()
          }
    • sync.Once

      ​   在实际工作中,可能会有这样的需求:让代码只执行一次,哪怕是高并发情况下,比如创建一个单例。针对这种情形,Go语言为我们提供了sync.Once来保证代码只执行一次。

      ​   这是Go语言自带的一个示例,虽然启动了10个协程来执行onceBody函数,但是因为用了once.Do方法,所以函数onceBody只会执行一次。也就是在高并发的情况下,sync.Once也会保证onceBody函数只执行一次。

      ​ sync.Once 适用于创建某个对象的单例、只加载一次的资源等只执行一次的场景。

      • func main() {
            doOnce()
        }
        
        func doOnce() {
            var once sync.Once 
            onceBody := func() {
                fmt.Println("Only once")
            }
            
            // 用于等待协程执行完毕
            done := make(chan bool)
            
            // 启动10个协程执行once.Do(onceBody)
            for i := 1; i < 10; i++ {
                go func(){
                    go func() {
                        // 把要执行的函数(方法)作为参数传给once.Do方法即可
                        once.Do(onceBody)
                        done <- true
                    }
                }()
            }
            
            for i := 10; i < 10 ; i++{
                <- done
            }
        }
    • sync.Cond

      ​   在Go语言中,sync.WaitGroup用于最终完成的场景,关键点在于一定要等待所有协程都执行完毕。而sync.Cond 可以用于发号施令,一声令下所有协程都可以开始执行,关键点在于协程开始的时候是等待的,要等待sync.Cond唤醒才能执行。

      ​         sync.Cond从字面意思看是条件变量,它具有阻塞协程和唤醒协程的功能,所有可以在满足一定条件的情况下唤醒协程,但条件变量只是它的一种使用场景。

      • // 10个人赛跑,1人裁判发号施令
        func race() {
            cond := sync.NewCond(&sync.Mutex{})
            var wg sync.WaitGroup
            wg.Add(11)
            for i := 1; i < 10; i++ {
                go func(num int) {
                    defer wg.Done() 
                    fmt.Println(num, "号已就位")
                    cond.L.Lock()
                    cond.Wait() // 等待发令枪响
                    fmt.Println(num, "号开始跑")
                    cond.L.Unlock()
                }(i)
            }
            
            time.Sleep(2 * time.Second)
            go func(){
                defer wg.Done()
                fmt.Println("裁判已就位,准备发令枪")
                fmt.Println("比赛开始,大家开始跑")
                cond.Broadcast() // 发令枪响
            }()
            
            wg.Wait()
        }
      • 大概步骤:

        • 通过sync.NewCond函数生成一个*sync.Cond,用于阻塞和唤醒协程

        • 然后启动10个协程模拟10个人,准备就位后调用cond.Wait()方法阻塞当前协程等待发令枪响,这里需要注意的是调用cond.Wait()方法是要加锁

        • time.Sleep 用于等待所有人都进入wait状态,这样裁判才能调用cond.Broadcast()发号施令

        • 裁判准备完毕后,可以调用cond.Broadcast通知所有人开始跑了

      • sync.Cond 有三个方法,分别是:

        • Wait,阻塞当前协程,知道被其他协程调用Broadcast 或者 Signal 方法唤醒,使用的时候需要加锁,使用sync.Cond中的锁即可,也就是L字段

        • **Signal **,唤醒一个等待时间最长的协程

        • Broadcast,唤醒所有等待的协程

      • 注意:在调用signal 或者Broadcast之前,要确保目标协程处于Wait阻塞状态,不然会出现死锁问题。

 

  • channel

      channel 是 Go 中定义的一种类型,专门用来在多个 goroutine 之间通信的线程安全的数据结构。

      可以在一个 goroutine 中向一个 channel 中发送数据,从另外一个 goroutine 中接收数据。

    channel 类似队列,满足先进先出原则。

    • 定义

      • // 仅声明
        var <channel_name> chan <type_name>
        
        // 初始化
        <channel_name> := make(chan <type_name>)
        
        // 初始化有缓冲的channel
        <channel_name> := make(chan <type_name>, 3)
    • channel 的三种操作:发送数据,接收数据,以及关闭通道。

      • // 发送数据
        channel_name <- variable_name_or_value
        
        // 接收数据
        value_name, ok_flag := <- channel_name
        value_name := <- channel_name
        
        // 关闭channel
        close(channel_name)
    • channel 还有两个变种

       可以把 channel 作为参数传递时,限制 channel 在函数或方法中能够执行的操作。

      • //仅发送数据
        func <method_name>(<channel_name> chan <- <type>)
        
        //仅接收数据
        func <method_name>(<channel_name> <-chan <type>)
    • 示例

      • package main
        
        import (
            "fmt"
            "time"
        )
        
        // 只接收channel的函数
        func receiveOnly(ch <-chan int) {
            for v := range ch {
                fmt.Printf("接收到: %d\n", v)
            }
        }
        
        // 只发送channel的函数
        func sendOnly(ch chan<- int) {
            for i := 0; i < 5; i++ {
                ch <- i
                fmt.Printf("发送: %d\n", i)
            }
            close(ch)
        }
        
        func main() {
            // 创建一个带缓冲的channel
            ch := make(chan int, 3)
        
            // 启动发送goroutine
            go sendOnly(ch)
        
            // 启动接收goroutine
            go receiveOnly(ch)
        
            // 使用select进行多路复用
            timeout := time.After(2 * time.Second)
            for {
                select {
                case v, ok := <-ch:
                    if !ok {
                        fmt.Println("Channel已关闭")
                        return
                    }
                    fmt.Printf("主goroutine接收到: %d\n", v)
                case <-timeout:
                    fmt.Println("操作超时")
                    return
                default:
                    fmt.Println("没有数据,等待中...")
                    time.Sleep(500 * time.Millisecond)
                }
            }
        }
  • 锁与 channel

  在 Go 中,当需要 goroutine 之间协作地方,更常见的方式是使用 channel,而不是 sync 包中的 Mutex 或 RWMutex 的互斥锁。但其实它们各有侧重。大部分时候,流程是根据数据驱动的,channel 会被使用得更频繁。

    • channel 擅长的是数据流动的场景:

    1. 传递数据的所有权,即把某个数据发送给其他协程。
    2. 分发任务,每个任务都是一个数据。
    3. 交流异步结果,结果是一个数据。
    • 而锁使用的场景更偏向同一时间只给一个协程访问数据的权限:

    1. 访问缓存
    2. 管理状态

select

  select 语义是和 channel 绑定在一起使用的,select 可以实现从多个 channel 收发数据,可以使得一个 goroutine 就可以处理多个 channel 的通信。

  语法上和 switch 类似,有 case 分支和 default 分支,只不过 select 的每个 case 后面跟的是 channel 的收发操作。

  • 定义

    • select {
      case channel_name <- varaible_name_or_value: 
          do sth
      case value_name, ok_flag := <- channel_name:
          do sth
      default:
          do sth
      }

      语法上和switch的一些区别:

      • select 关键字和后面的 { 之间,不能有表达式或者语句。
      • 每个 case 关键字后面跟的必须是 channel 的发送或者接收操作
      • 允许多个 case 分支使用相同的 channel,case 分支后的语句甚至可以重复
  • 示例

    • package main
      
      import (
          "fmt"
      )
      
      func main() {
          ch1 := make(chan int, 10)
          ch2 := make(chan int, 10)
          ch3 := make(chan int, 10)
          go func() {
              for i := 0; i < 10; i++ {
                  ch1 <- i
                  ch2 <- i
                  ch3 <- i
              }
          }()
          for i := 0; i < 10; i++ {
              select {
              case x := <-ch1:
                  fmt.Printf("receive %d from channel 1\n", x)
              case y := <-ch2:
                  fmt.Printf("receive %d from channel 2\n", y)
              case z := <-ch3:
                  fmt.Printf("receive %d from channel 3\n", z)
              }
          }
      }

    1. 在执行 select 语句的时候,如果当下那个时间点没有一个 case 满足条件,就会走 default 分支。

    2. 至多只能有一个 default 分支。

    3. 如果没有 default 分支,select 语句就会阻塞,直到某一个 case 满足条件。

    4. 如果 select 里任何 case 和 default 分支都没有,就会一直阻塞。

    5. 如果多个 case 同时满足,select 会随机选一个 case 执行。

编译可执行文件

  • 无参编译

    • 如果声明 main 函数的文件就在项目目录下,只需要在项目根目录下执行 go build 命令:

      go build

      Go 就会根据当前目录下声明的 go.mod 文件中,声明的 module 名称,把当前项目的执行文件输出到 module 名称同名的文件中。

    • 如果想要修改路径和编译出来的可执行文件的名称,还需要添加 -o 参数:

      go build -o <output_path>

 

    • 另外,Go 的工程标准,一般会把声明 main 函数的文件放到项目目录的 cmd 目录下,所以需要再加上这个文件的路径:
go build -o <output_path_with_file_name> <go_file_path>
  • 交叉编译

    • 而为了让程序在不同平台下正常运行,Go 还让 build 命令在执行时,可以读到环境变量中的一些参数以及 tags 参数。

      其中两比较重要的变量分别是:

      • GOOS:指定运行操作系统,目前支持 linuxwindows 和 darwin(MacOS),三种类型。
      • GOARCH:指定运行的架构,可以配置 amd64arm64i386 等。
      • CGO_ENABLED:如果项目依赖于另外的 C 库或其他工具,就需要在交叉编译时配置 CGO_ENABLED 变量,默认情况下是 0,也就是关闭,当为 1 时,表示开启。

      当需要使用到这些变量时,可以配置成环境变量,也可以直接在命令中定义,即可开始编译:

      //编译 Linux 64位
      GOOS=linux GOARCH=amd64 go build -o demo-amd64 main.go
      
      //编译 Windows 64位
      GOOS=windows GOARCH=amd64 go build -o demo-windows-amd64.exe main.go
      
      //编译 macOS 64位
      GOOS=darwin GOARCH=amd64 go build -o demo-darwin-amd64 main.go
      
      //编译 Linux 32位
      GOOS=linux GOARCH=i386 go build -o myapp-linux-386 main.go
  •  条件编译

    • 概念

 

  有时候在不同架构或系统下,由于各种各样的原因,不同系统提供的函数是不同的,那么在为了保证在不同系统下,程序能够正常运行。又或者公司的项目,为了区分收费版与免费版的程序,也可以使用条件编译的方式,让程序能够在一个项目下,编译出不同行为的可执行文件。

    • 示例

      • 创建 func_linux.go 文件,以下是文件代码
      • // +build linux
        package main
        
        import "fmt"
        
        func platformSpecificFunction() {
            fmt.Println("This is the Linux implementation.")
        }
      • 创建 func_windows.go 文件:
      • // +build windows
        package main
        
        import "fmt"
        func platformSpecificFunction() {
            fmt.Println("This is the Windows implementation.")
        }

         

      • 创建 func_darwin.go 文件:

      • // +build darwin
        package main
        
        import "fmt"
        func platformSpecificFunction() {
            fmt.Println("This is the Darwin implementation.")
        }

      在 go1.17 版本以上,上面这几个文件的代码会被 IDE 自动添加一行代码://go:build xxx,这是 go 新引入的条件编译的格式,是为了替换 + build

在 main.go 中,可以直接调用 platformSpecificFunction

      • package main
        
        func main() {
            platformSpecificFunction()
        }    

可以直接使用 go build 命令编译并执行。

这里添加 tags 参数,是因为 linuxwindows 以及 darwin 这三个 tag 比较特殊,会根据 GOARCH 自动添加。

假如把 tag 名称改成其他的值,就需要在编译时特意指定,定义格式:

        • go build -tags="<tag1> <tag2> <...>"
  • 条件编译的 tag 逻辑

在一个包中使用多个标签时,它们会使用布尔逻辑进行交互,具体取决于我们如何进行声明。

Build tags 遵循以下三个规则:

    • 以空格分隔的标签将在 OR 逻辑下进行解释。

      •  +build tag1 tag2

当编译时的 tags 参数只要声明了 tag1、tag2 这两个 tag 值任意一个,则编译时包含此文件

    • 逗号分隔的标签将在 AND 逻辑下进行解释。

      • +build tag1, tag2
        或者
        // +build tag1
        // +build tag2

        当编译时的 tags 参数需要同时声明 tag1 和 tag2 时,这个 go 文件才能包含在编译中。

    • 每个术语都是一个字母数字单词,如果前面有! 它意味着它被否定。

      • // +build !tag1

        当编译时的 tags 参数没有 tag1 的值时,这个 go 文件才会 build 编译。

    • 新版 Tag 逻辑关系

      新版为了让 tag 之间的逻辑关系更加明显且容易辨认与编写, 简化了 tag 声明,并且为了与老版 tag 声明区分,修改的声明格式:

      • // go:build linux && amd64 || darwin

        从 + build 变成了 go:build,并且逻辑关系也更加明显,直接使用逻辑运算符来声明 tag 之间的逻辑关系。目前当声明旧的条件编译条件时,go 的开发环境工具会帮我们自动生成新版 tag 的声明。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2025-10-20 23:43  直至成伤  阅读(12)  评论(0)    收藏  举报