golang 基础案例_02

发布于:2025-08-12 ⋅ 阅读:(20) ⋅ 点赞:(0)

1.锁

        有时候我们的代码中可能会存在多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)。

        (1)、互斥锁;(2)、读写互斥锁;(3)、sync.WaitGroup;(4)、sync.Once;(5)、sync.Map;(6)、atomic包

var (
    x       int64
    m       sync.Mutex // 互斥锁
    rwMutex sync.RWMutex
    mutex   sync.Mutex
)
 
func Mutex() {
    wg.Add(2)
 
    go add()
    go add()
 
    wg.Wait()
    fmt.Println(x)
}
 
func add() {
    for i := 0; i < 5000; i++ {
       m.Lock() // 修改x前加锁
       x = x + 1
       m.Unlock() // 改完解锁
    }
    wg.Done()
}
 
func RWMutex() {
    /*
       读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
       而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
    */
    // 使用互斥锁,10并发写,1000并发读
    do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s
    // 使用读写互斥锁,10并发写,1000并发读
    do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms
 
    /*
       从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。
       不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来。
    */
}
 
// writeWithLock 使用互斥锁的写操作
func writeWithLock() {
    mutex.Lock() // 加互斥锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    mutex.Unlock()                    // 解互斥锁
    wg.Done()
}
 
// readWithLock 使用互斥锁的读操作
func readWithLock() {
    mutex.Lock()                 // 加互斥锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    mutex.Unlock()               // 释放互斥锁
    wg.Done()
}
 
// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {
    rwMutex.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
    rwMutex.Unlock()                  // 释放写锁
    wg.Done()
}
 
// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
    rwMutex.RLock()              // 加读锁
    time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
    rwMutex.RUnlock()            // 释放读锁
    wg.Done()
}
 
func do(wf, rf func(), wc, rc int) {
    start := time.Now()
    // wc个并发写操作
    for i := 0; i < wc; i++ {
       wg.Add(1)
       go wf()
    }
    //  rc个并发读操作
    for i := 0; i < rc; i++ {
       wg.Add(1)
       go rf()
    }
    wg.Wait()
    cost := time.Since(start)
    fmt.Printf("x:%v cost:%v\n", x, cost)
}


func WaitGroup() {
    /*
          WaitGroup
       在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:
       方法名    功能
       func (wg * WaitGroup) Add(delta int)   计数器+delta
       (wg *WaitGroup) Done() 计数器-1
       (wg *WaitGroup) Wait() 阻塞直到计数器变为0
       sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done 方法将计数器减1。通过调用 Wait 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。
    */
 
    wg.Add(1)
    go hello()
    wg.Wait()
    fmt.Println("ni hao ya !!!")
}
func hello() {
    defer wg.Done()
    fmt.Println("hello world....")
}

2.SyncOnce

        在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。
          Go语言中的sync包中提供了一个针对只执行一次场景的解决方案——sync.Once,sync.Once只有一个Do方法,
          延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量
       (比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。

func SyncOnce() {
    GetInstance() //并发安全的单例模式
}
 
type singleton struct{}
 
var instance *singleton
var once sync.Once
 
func GetInstance() *singleton {
    once.Do(func() {
       instance = &singleton{}
    })
    return instance
}

3.并发安全的map

        sync.Map 是 Go 语言中提供的一个并发安全的 map 类型,它是 Go 语言中 map 的替代品,它是并发安全的,并且它是通过引入 sync.RWMutex 来实现的。
       在 Go 语言中,map 是无序的,而 sync.Map 是基于 sync.RWMutex 实现的,所以它是并发安全的。
       sync.Map 的底层实现是通过哈希表来实现的,哈希表的底层实现是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来实现的,所以它是通过数组+链表来。


var smp = sync.Map{}
 
func SyncMap() {
    wg := sync.WaitGroup{}
    // 对m执行20个并发的读写操作
    for i := 0; i < 20; i++ {
       wg.Add(1)
       go func(n int) {
          key := strconv.Itoa(n)
          smp.Store(key, n)         // 存储key-value
          value, _ := smp.Load(key) // 根据key取值
          fmt.Printf("k=:%v,v:=%v\n", key, value)
          wg.Done()
       }(i)
    }
    wg.Wait()
}

4.atomic

        atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。
        除了某些特殊的底层应用,使用通道或者 sync 包的函数/类型实现同步更好。
        针对整数数据类型(int32、uint32、int64、uint64)我们还可以使用原子操作来保证并发安全,通常直接使用原子操作比使用锁操作效率更高。Go语言中原子操作由内置的标准库sync/atomic提供
    func (o *TrinoQueryRunningExporter) AcquireLock() bool {
       return atomic.CompareAndSwapInt64(&o.Lock, 0, 1)
    }
    func (o *TrinoQueryRunningExporter) ReleaseLock() {
       atomic.CompareAndSwapInt64(&o.Lock, 1, 0)
    }
这里的锁就是原子操作的使用案例。

func Atomic() {
    c1 := CommonCounter{} // 非并发安全
    test(c1)
    c2 := MutexCounter{} // 使用互斥锁实现并发安全
    test(&c2)
    c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
    test(&c3)
}
 
type Counter interface {
    Inc()
    Load() int64
}
 
// 普通版
type CommonCounter struct {
    counter int64
}
 
func (c CommonCounter) Inc() {
    c.counter++
}
 
func (c CommonCounter) Load() int64 {
    return c.counter
}
 
// 互斥锁版
type MutexCounter struct {
    counter int64
    lock    sync.Mutex
}
 
func (m *MutexCounter) Inc() {
    m.lock.Lock()
    defer m.lock.Unlock()
    m.counter++
}
 
func (m *MutexCounter) Load() int64 {
    m.lock.Lock()
    defer m.lock.Unlock()
    return m.counter
}
 
// 原子操作版
type AtomicCounter struct {
    counter int64
}
 
func (a *AtomicCounter) Inc() {
    atomic.AddInt64(&a.counter, 1)
}
 
func (a *AtomicCounter) Load() int64 {
    return atomic.LoadInt64(&a.counter)
}
 
func test(c Counter) {
    var wg sync.WaitGroup
    start := time.Now()
    for i := 0; i < 1000; i++ {
       wg.Add(1)
       go func() {
          c.Inc()
          wg.Done()
       }()
    }
    wg.Wait()
    end := time.Now()
    fmt.Println(c.Load(), end.Sub(start))
}

5.error

        Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch捕获异常的方式
        Go 语言中使用一个名为 error 接口来表示错误类型。
        error 接口只包含一个方法——Error,这个函数需要返回一个描述错误信息的字符串。当一个函数或方法需要返回错误时,我们通常是把错误作为最后一个返回值。
        我们可以根据需求自定义 error,最简单的方式是使用errors 包提供的New函数创建一个错误。
        errors.New
        当我们需要传入格式化的错误描述信息时,使用fmt.Errorf是个更好的选择。


func ErrorNew() {
    id := -1
    var err error
    if id < 0 {
       err = errors.New("无效的id")
       fmt.Printf("id error:%v \n", err)
    }
    fmt.Println(fmt.Errorf("查询数据库失败,v err:%v \n", err))
    //但是上面的方式会丢失原有的错误类型,只拿到错误描述的文本信息。
    //为了不丢失函数调用的错误链,使用fmt.Errorf时搭配使用特殊的格式化动词%w,可以实现基于已有的错误再包装得到一个新的错误。
    fmt.Println(fmt.Errorf("查询数据库失败,w err:%w \n", err))
 
    //自定义结构体类型,可以自己定义结构体类型,实现 error`接口
 
    oper := &OpError{
       Op: "update",
    }
    fmt.Println(oper.Error())
}
 
// Error OpError 类型实现error接口
func (e *OpError) Error() string {
    return fmt.Sprintf("无权执行%s操作", e.Op)
}
 
// OpError 自定义结构体类型
type OpError struct {
    Op string
}

6.类型转换

        strconv包实现了基本数据类型与其字符串表示的转换,主要有以下常用函数: Atoi()、Itoa()、parse系列、format系列、append系列。
        【扩展阅读】这是C语言遗留下的典故。C语言中没有string类型而是用字符数组(array)表示字符串,所以Itoa对很多C系的程序员很好理解。
        strconv.Atoi  strconv.Itoa
        Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、        ParseUint()。
        Format系列函数实现了将给定类型数据格式化为string类型数据的功能

func TypeTransfer() {
    s1 := "100"
    i1, err := strconv.Atoi(s1) // Atoi()函数用于将字符串类型的整数转换为int类型
    if err != nil {
       fmt.Println("can't convert to int")
    } else {
       fmt.Printf("type:%T value:%#v\n", i1, i1) //type:int value:100
    }
 
    v := int64(-42)
    s10 := strconv.FormatInt(v, 10)
    fmt.Printf("%T, %v\n", s10, s10)
    s16 := strconv.FormatInt(v, 16)
    fmt.Printf("%T, %v\n", s16, s16)
}

7.ini

        在Go语言中,init()函数是一种特殊的函数,用于在程序启动时自动执行一次。它的存在为我们提供了一种机制,可以在程序启动时进行一些必要的初始化操作,为程序的正常运行做好准备
go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。
       (1) init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
        (2) 每个包可以拥有多个init函数
        (3) 包的每个源文件也可以拥有多个init函数
        (4) 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
        (5) 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
        (6) init函数不能被其他函数调用,而是在main函数执行之前,自动被调用
init函数和main函数的异同:
相同点:
    两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
不同点:
    init可以应用于任意包中,且可以重复定义多个。
    main函数只能用于main包中,且只能定义一个。
go中包的初始化顺序:
首先初始化包内声明的变量
之后调用 init 函数
最后调用 main 函数
两个函数的执行顺序:
对同一个go文件的init()调用顺序是从上到下的。
对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。
对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。
如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。
https://www.cnblogs.com/chenjiazhan/p/17473207.html
https://www.cnblogs.com/XiaoXiaoShuai-/p/14642055.html
init 函数的用途
(1) 初始化全局变量
(2) 执行一些必要的验证操作
注意:
init 函数不能被显式调用
init 函数只执行一次
避免在 init 函数中执行耗时操作


func Init() {
    fmt.Println("Init Test!!!")
}
 
func init() {
    fmt.Println("hello world")
}

8.test

        Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
        go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
        类型  格式 作用
        测试函数    函数名前缀为Test 测试程序的一些逻辑行为是否正确
        基准函数    函数名前缀为Benchmark    测试函数的性能
        示例函数    函数名前缀为Example  为文档提供示例文档
        go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

func TestChannel() {
    /*
       第一次循环时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以x := <-ch 这个 case 分支不满足,而ch <- i这个分支可以执行,会把1发送到通道中,结束本次 for 循环;
       第二次 for 循环时,i = 2,由于通道缓冲区已满,所以ch <- i这个分支不满足,而x := <-ch这个分支可以执行,从通道接收值1并赋值给变量 x ,所以会在终端打印出 1;
       后续的 for 循环以此类推会依次打印出3、5、7、9。
    */
    ch := make(chan int, 1)
    for i := 1; i <= 10; i++ {
       select {
       case x := <-ch:
          fmt.Println(x)
       case ch <- i:
       }
    }
}

9.指针

        Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。


func Pointer() {
    a := 100
    b := &a
    fmt.Printf("a:%v, b:%v,bp:%p \n", a, b, b)
    fmt.Printf("&b:%v,\n", &b)
    modValue(b)
    fmt.Printf("a:%v, b:%v,bp:%p \n", a, b, b)
}
 
func modValue(i *int) {
 
    if i != nil {
       *i += 101
    }
}
 

10.context

 
func Context() {
    wg.Add(2)
    go func() {
       time.Sleep(time.Second * 2)
       fmt.Println("job 1 done")
       wg.Done()
    }()
    go func() {
       time.Sleep(time.Second * 1)
       fmt.Println("job 2 done")
       wg.Done()
    }()
    wg.Wait()
    fmt.Println("all job done")
}
 
func Context2() {
    stop := make(chan bool)
    go func() {
       for {
          select {
          case <-stop:
             fmt.Println("got the stop channel")
             return
          default:
             fmt.Println("still working")
             time.Sleep(time.Second * 1)
          }
       }
    }()
    time.Sleep(time.Second * 5)
    fmt.Println("stop the goroutine")
    stop <- true
    time.Sleep(time.Second * 5)
}
 
func Context3() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, "worker1")
    go worker(ctx, "worker2")
    go worker(ctx, "worker3")
    time.Sleep(time.Second * 5)
    fmt.Println("stop the goroutine")
    cancel()
    time.Sleep(time.Second * 5)
}
 
func worker(ctx context.Context, name string) {
    go func() {
       for {
          select {
          case <-ctx.Done():
             fmt.Println("got the stop channel")
             return
          default:
             fmt.Println(name, " still working")
             time.Sleep(time.Second * 1)
          }
       }
    }()
}
 
func Context4() {
    // 创建一个带有取消功能的上下文
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
 
    // 设置一个截止时间为5秒后
    ctx, cancel = context.WithDeadline(ctx, time.Now().Add(5*time.Second))
    defer cancel()
 
    // 向上下文中添加一个值
    ctx = context.WithValue(ctx, "key", "value")
 
    // 启动一个goroutine来监听上下文的取消信号
    go func() {
       select {
       case <-ctx.Done():
          fmt.Println("Context done:", ctx.Err())
       }
    }()
 
    // 启动一个goroutine来获取上下文中的值
    go func() {
       time.Sleep(2 * time.Second)
       value := ctx.Value("key")
       fmt.Println("Context value:", value)
    }()
 
    // 模拟一个耗时操作
    select {
    case <-time.After(10 * time.Second):
       fmt.Println("Operation completed")
    case <-ctx.Done():
       fmt.Println("Operation canceled due to context")
    }
 
    // 获取上下文的截止时间
    if deadline, ok := ctx.Deadline(); ok {
       fmt.Println("Context deadline:", deadline)
    } else {
       fmt.Println("No deadline set for context")
    }
}
 
func ContextWaitGroup() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
 
    var wg sync.WaitGroup
 
    wg.Add(1)
    go func() {
       defer wg.Done()
       for {
          select {
          case <-ctx.Done():
             fmt.Println("Goroutine canceled due to context")
             return
          default:
             // 模拟一些工作
             fmt.Println("Goroutine working...")
             time.Sleep(1 * time.Second)
          }
       }
    }()
 
    wg.Wait()
    fmt.Println("Main goroutine finished")
}
 
func ContextWaitGroup1() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
 
    go func() {
       for {
          select {
          case <-ctx.Done():
             fmt.Println("goroutine exiting...")
             return
          default:
             fmt.Println("goroutine working...")
             time.Sleep(500 * time.Millisecond)
          }
       }
    }()
 
    time.Sleep(3 * time.Second)
    fmt.Println("main function exiting...")
}

11.闭包

        (Closure)是一种特殊的函数,它可以捕获其创建时所在作用域中的变量。闭包通常与匿名函数一起使用,匿名函数可以访问并操作不在其参数列表中的外部变量。
        Go语言中的闭包有几个特殊的用途和优势:
        状态封装,控制变量生命周期,函数工厂,实现回调和延后执行,模块化和封装,实现接口,高阶函数,迭代器和生成器,避免命名冲突;
        使用闭包的注意事项:内存泄漏,并发安全,循环引用.

func Closure() {
    /*
          这里这里start := time.Now() 已经运行了
       返回的函数再跟后面做减法
    */
    defer TimeCost("test closure", 11)()
    time.Sleep(time.Second * 2)
}
 
func TimeCost(handlerName string, req ...interface{}) func() {
    fmt.Printf(fmt.Sprintf("TimeCost for %s start now.", handlerName))
    start := time.Now()
    return func() {
       tc := time.Since(start)
       fmt.Printf(fmt.Sprintf("handle %s for request:%+v time cost is:%+v", handlerName, req, tc))
    }
}

12.http\UDP

                UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。

func process(conn net.Conn) {
    defer conn.Close()
    for {
       reader := bufio.NewReader(conn)
       var buf [128]byte
       n, err := reader.Read(buf[:])
       if err != nil {
          fmt.Printf("read failed,err:%v", err)
          break
       }
       recv := string(buf[:n])
       fmt.Printf("接收到的数据:%v", recv)
       conn.Write([]byte("ok"))
    }
}
 
func HttpServer() {
    listen, err := net.Listen("tcp", "127.0.0.1:8801")
    if err != nil {
       fmt.Printf("listen failed,err:%v", err)
       return
    }
    for {
       conn, err := listen.Accept()
       if err != nil {
          fmt.Printf("Accept failed ,err:%v", err)
          continue
       }
       go process(conn)
    }
}
 
func HttpClinet() {
    conn, err := net.Dial("tcp", "127.0.0.1:8801")
    if err != nil {
       fmt.Printf("connect failed,err:%v", err)
       return
    }
    input := bufio.NewReader(os.Stdin)
    for {
       s, _ := input.ReadString('\n')
       s = strings.TrimSpace(s)
       if strings.ToUpper(s) == "Q" {
          return
       }
       //给服务端发消息
       _, err := conn.Write([]byte(s))
       if err != nil {
          fmt.Printf("send failed,err:%v \n", err)
          return
       }
       //从服务端接收消息
       var buf [1023]byte
       n, err := conn.Read(buf[:])
       if err != nil {
          fmt.Printf("read failed,err:%v \n", err)
          return
       }
       fmt.Printf("收到服务单回复:%v", string(buf[:n]))
    }
}
 
/*
RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。要实现RPC就需要解决以下三个问题。
如何确定要执行的函数?调用方和被调用方都需要维护一个{ function <-> ID }映射表,以确保调用正确的函数
如何表达参数? 参数或返回值需要在传输期间序列化并转换成字节流,反之亦然
如何进行网络传输?只要能够完成传输,调用方和被调用方就不受某个网络协议的限制
*/