标签 golang 下的文章

GVM(Go Version Manager)是一款用于管理和切换不同Go语言版本的工具。它允许用户在同一台计算机上轻松安装、使用和管理多个Go版本,同时还能确保项目之间的依赖关系井然有序。GVM的主要功能包括:

  1. 安装和卸载Go版本:GVM允许用户快速安装和卸载Go语言的不同版本,以便在不同项目中使用。
  2. 切换Go版本:GVM可以轻松切换当前正在使用的Go版本,这对于在不同项目中使用不同Go版本的开发者来说非常有用。
  3. 设置默认Go版本:GVM允许用户设置一个默认的Go版本,以便在新的终端会话中自动使用。
  4. 管理Go的环境变量:GVM可以自动管理Go的环境变量,如GOROOTGOPATH,以确保每个Go版本的正确配置。
  5. 支持离线安装:GVM支持通过本地二进制包进行Go语言的安装,这对于无法访问Go官方网站的用户来说非常有帮助。

通过GVM,开发者可以更方便地在不同版本的Go语言之间进行切换,从而提高开发效率和降低潜在的兼容性问题。

问题:GVM工具默认从官网下载安装包安装,因为众所周知的原因,国内网络无法访问到golang官网,因此下载安装包会失败。包括安装gvm工具本身也是一样。

如何安装gvm

首先将gvm安装脚本内容拷贝到本地,使用可以访问外网的浏览器打开:

https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer

然后执行以下命令安装:

yum install bison
# 设置安装的代码源仓库,默认是github,在国内大概率拉不下来,因此需要拉到国内
export SRC_REPO=https://gitee.com/voidint/gvm.git
# 安装
./gvm_install.sh

用法:

# 查看所有版本
gvm list
# 使用指定版本
gvm use go1.18
# 设置默认的版本
gvm use go1.18 --default

如何基于本地包安装go

gvm默认使用golang官网来下载二进制,但是国内无法访问golang官网,所以是无法安装成功。gvm提供了通过离线二进制包安装的能力,可以在国内golang网站下载好二进制包,放到~/.gvm/archive/目录下,然后执行命令安装:

gvm install go1.21.5 --binary

这样就解决了网络不通的问题。

注意:版本号必须要匹配

一、简介

go module公共代理仓库,代理并缓存go模块。你可以利用该代理来避免DNS污染导致的模块拉取缓慢或失败的问题,加速你的构建。

简单来说就是国内访问被墙,go get无法在线获取到仓库,代理仓库就是帮我们解决这个问题的。

二、使用方法

  1. 使用go1.11以上版本并开启go module机制
  2. 导出GOPROXY环境变量
export GOPROXY=https://xxxx.xx

三、国内优质代理仓库

3.1 七牛

export GOPROXY=https://goproxy.cn

3.2 阿里云

export GOPROXY=https://mirrors.aliyun.com/goproxy/

一、关于glog库

golang中的glog库是google著名开源C++日志库glog的golang版本,在golang默认日志库的基础上做了更进一层的封装使得该库能更贴近日常使用。项目首页为golang/glog,当前版本的glog有以下几个特点:

  1. 支持四种日志级别:INFO < WARNING < ERROR < FATAL,支持不同级别的日志打印到不同文件中。
  2. 默认情况下日志不是打印到标准输出或标准错误,需要手动添加参数才能把日志打到标准错误中。
  3. 支持根据文件大小来切割文件,但是不支持根据日期切割。
  4. 日志输出的格式固定且不可修改:Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg,其中L表示日志级别首字符,如Info日志,L打印出来是ID`。
  5. 源码实现简单,方便自主修改。

安装glog:

go get github.com/golang/glog

1.1 基本用法

glog依赖命令行参数输入,默认是打印到文件的,在使用该库前必须先使用flag包解析命令行参数:

flag.Parse()

因为glog底层实现是先把日志输入到缓冲区buffer中,然后定时写入文件,所以在执行完日志输出后要手动执行glog.Flush()确保日志刷新到文件。一般在函数开始处执行:

defer glog.Flush()

输出日志时,需要加上-log_dir参数以指定目录写入日志文件,如以下代码:

package main

import (
    "flag"
    "github.com/golang/glog"
)

func main() {
    flag.Parse()
    defer glog.Flush()

    glog.Info("HelloWorld")
}

编译后执行:

./glog -log_dir=log

在log目录下会生成相应的日志文件,查看对应的日志:

打印到标准输出

默认情况下,日志不是打印到标准输出中,如需打印到标准输出可以使用以下两个参数:

  1. logtostderr:打印到标准错误而不是文件。
  2. alsologtostderr:同时打印到标准错误。

这两个参数都会把日志打印到标准错误中(在linux终端环境下,前台显示的标准错误和标准输出可以认为是同一个输出),调试日志时可以加上这两个参数中的任一:

二、日志级别

v levelv module功能是最常用到的功能,适用于给同一套代码在不同环境下设置不同日志级别的功能。如在调试环境下打印出更多级别的日志,生产环境下打印更少的日志。

2.1 打印特定级别的日志

使用方法:

glog.V(n).Info("Log message")

其中n表示的是日志级别,当n小于等于设定日志级别时将会被打印。默认情况下日志级别时0,启动时通过-v=x参数设定日志级别。如在代码中分别以级别3和级别5打印两条日志:

func main() {
    flag.Parse()
    defer glog.Flush()

    glog.V(3).Info("Level 3 log")
    glog.V(5).Info("Level 5 log")
}

执行时设定日志级别为4,日志将不会打印出第二条级别为5的日志:

./glog -v=4 -log_dir=log

日志:

Log file created at: 2019/09/08 18:04:55
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:04:55.924127     546 main.go:12] Level 3 log

2.2 vmodule选项

vmodule选项可以在上面设定v level之后,单独再给某个文件设置日志级别,适用于以下场景:

  • 系统默认日志级别为3,但是希望main.go中的日志级别为5

执行:./glog -v=4 -log_dir=log -vmodule=main=5,日志中将会打印出级别为5的日志:

Log file created at: 2019/09/08 18:11:36
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:11:36.096301     557 main.go:12] Level 3 log
I0908 18:11:36.100119     557 main.go:13] Level 5 log

三、自定义修改

3.1 修改日志WARN为DEBUG

默认是没有DEBUG日志的,WARN日志用得少可以把WARN日志改成DEBUG。在glog.go源码文件中,修改以下内容:

const severityChar = "IWEF" // 改为"IDEF"

var severityName = []string{
    infoLog:  "INFO",
    warningLog: "WARN" // 改为"DEBUG",
    errorLog: "ERROR",
    fatalLog: "FATAL",
}

这里实现简单,改起来也简单。

3.2 默认打印到标准错误

// 搜索函数init
func init() {
    // 注释掉下面两行
    //flag.BoolVar(&logging.toStderr, "logtostderr", false, "log to standard error instead of files")
    //flag.BoolVar(&logging.alsoToStderr, "alsologtostderr", false, "log to standard error as well as files")
    
    // 添加下面两行
    logging.alsoToStderr = false
    logging.toStderr = true

    // Default stderrThreshold is ERROR.
    logging.stderrThreshold = errorLog

    logging.setVState(0, nil, false)
    go logging.flushDaemon()
}

一、waitgroup介绍

多线程编程中,经常会遇到这样的一种场景:main函数中为了等待其他线程执行完,在return之前都要执行sleep以争取更多的时间给其他线程执行。例如:

package main

import (
    "fmt"
    "time"
)

func main(){
    for i := 0; i < 100 ; i++{
        go fmt.Println(i)
    }
    time.Sleep(time.Second)
}

主线程为了等待goroutine都运行完毕,不得不在程序的末尾使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。对于简单的代码,100个for循环可以在1秒之内运行完毕,time.Sleep() 也可以达到想要的效果。但是对于实际场景来说,大多无法预知for循环内代码运行时间的长短,因此1秒可能是不够的。所以睡眠也就达不到我们想要的效果。

那么我们可能又会想到使用管道来完成同步,因为管道本身就是用来作为数据通信使用的,用在此处也合理。于是就有了这样的代码:

unc main() {
    c := make(chan bool, 100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            c <- true
        }(i)
    }

    for i := 0; i < 100; i++ {
        <-c
    }
}

首先可以肯定的是使用管道是能达到我们的目的的,但是问题是管道用在这里真的合适吗?管道是go中用来给多个线程(协程)间通信的,使用它来仅仅作为状态同步,是不是有点大材小用了。而且,管道是基于共享内存实现的,假设我们有一万、十万甚至更多的for循环,也要申请同样数量大小的管道出来,对系统性能也会造成更多的负载。

WaitGroup(中文名叫等待组)就是用来解决这种问题的,比较适合统筹多个协程间都到达某个状态。例如,等待所有协程都执行完这个状态。在WaitGroup 对象实现中,内部有一个计数器,最初从0开始,它有三个方法:

  • Add():计数器加一
  • Done():计数器减一
  • Wait():等待计数器清零

执行Wait方法的函数在等待组内部计数器不为0的时候回阻塞,一旦计数器为0了,程序就会继续往下执行。

利用WaitGroup实现上面的代码:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

程序刚开始首先把wg 计数设置为100,然后开启100个协程执行任务,每个协程执行完成之后把计数器减1,主线程中等待计数清零。当wg.Wait()继续向下执行了,也就说明所有的协程都执行完了。相较于管道来说,WaitGroup更简单,也更轻量。

二、注意事项

2.1 计数器不能为负值

使用等待组时注意不能通过Add()wg 设置一个负值,否则代码将会报错:

panic: sync: negative WaitGroup counter

goroutine 1 [running]:
sync.(*WaitGroup).Add(0xc000014060, 0xffffffffffffffff)
        /usr/local/go/src/sync/waitgroup.go:74 +0x139
main.main()
        /Users/maqian/code/go/src/awesomeProject/waitgroup/waitgroup.go:10 +0x4d

同样使用Done()也要特别注意不要把计数器设置成负数了。

2.2 WaitGroup对象不是一个引用类型

WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址:

// 一定要通过指针传值,不然进程会进入死锁状态
func f(i int, wg *sync.WaitGroup) {
    fmt.Println(i)
    wg.Done()
}

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go f(i, &wg)
    }
    wg.Wait()
}

一、问题描述

windows下,time.Parse()的时区和time.Format()的时区是一致的。

但是在linux环境下,time.Parse()的默认时区是UTCtime.Format()的时区默认是本地,两者如果不处理好就会导致错误。

package main
import "time"
import "fmt"
func main(){
    t, err := time.Parse("2006-01-02 15:04:05", "2017-12-03 22:01:02")
    if err != nil{
        fmt.Println(err)
        return
    }
    fmt.Println(t)
    fmt.Println(time.Now())
    fmt.Println(time.Now().Sub(t).Seconds())
}

输出:

2017-12-03 22:01:02 +0000 UTC
2017-12-03 22:15:26.592204446 +0800 CST m=+0.003020091
-27935.407549533

很明显能看到两者的时区不同并且如果把两者时间相减结果也不符合预期。

二、解决方法

使用time.ParseInLocation()而不是time.Parse()

package main

import "time"
import "fmt"

func main(){
    localTime, err := time.ParseInLocation("2006-01-02 15:04:05", "2017-12-03 22:01:02", time.Local)
    if err != nil{
        fmt.Println(err)
        return
    }
    fmt.Println(localTime)
    fmt.Println(time.Now())
    fmt.Println(time.Now().Sub(localTime).Seconds())
}

结果:

2017-12-03 22:01:02 +0800 CST
2017-12-03 22:18:26.288174547 +0800 CST m=+0.001532618
1044.288357362

一、channel

管道(channel)是golang中用于多协程通信的手段,也是go编程中常用到的数据类型。

虽然被称为管道,但是并非在《unix环境高级编程》中说的管道(fifo和pipe),go中的管道实际上是一种通过共享内存来实现的多线程通信方式,只是名字叫做管道而已。和fifo和pipe这两者没有任何关系。

创建一个管道的方法:

msg := make(chan string, 3)

其中的chan string表明管道是string类型,后面的3是管道容量。创建管道时如果指定了管道容量,管道就是一个有缓冲的管道。也可以省略3表示当前是一个无缓冲的管道,一般不建议使用无缓冲管道,可能导致程序阻塞。有缓冲管道和无缓冲管道的区别:

  1. 无缓冲管道:管道中没有元素的时候,读端会阻塞。管道有元素没有被读出的时候,写端阻塞。
  2. 有缓冲管道:管道中没有元素的嘶吼,读端会阻塞。管道中元素满了,写端阻塞。

管道的读写操作方法:

// 写管道
msg <- "HelloWorld"
// 读管道
val <- msg

示例

以下是一个无缓冲chan的执行示例,代码中分别创建两个goroutine,一个从管道读,一个往管道写,写端是在休眠一秒后才写。两个goroutine通过WaitGroup来同步结束状态:

func main() {
    var wg sync.WaitGroup
    msg := make(chan string)

    wg.Add(2)
    go func() {
        glog.Info("wait to msg...")
        glog.Info("receive msg: ", <-msg)
        wg.Done()
    }()

    go func() {
        time.Sleep(time.Second)
        msg <- "HelloWorld"
        wg.Done()
    }()

    wg.Wait()
}
代码中的输出使用glog来打印,方便输出更多信息。

输出结果:

管道接收方一共打印了两条日志,一条是协程刚启动准备接受消息时候的日志,一条是收到消息后的日志。对比两条日志的打印时间能看到中间间隔了1秒,这一秒刚好是管道发送方在睡眠,说明接收方在这一秒是在等待发送方发送数据,没有数据的时候被阻塞了。

使用无缓冲区管道千万要注意的就是阻塞,如果处理不当,读端没有及时读或者读端挂了,很可能就导致业务阻塞。

二、基于管道的异步调用

高并发场景中,一种经常用到的处理操作是异步调用,如何通过管道来实现异步调用呢?

以下是一段基于管道实现的异步调用示例,代码模拟了一次异步任务处理过程:主线程需要处理一个任务(耗时1秒),同时还要从数据库中读取一个字符串的数据。

func main() {
    var wg sync.WaitGroup
    msg := make(chan string)

    wg.Add(1)

    go func() { // 创建新的协程从数据库获取字符串
        glog.Info("service start!")
        msg <- "HelloWorld"
        glog.Info("service done!")
        wg.Done()
    }()

    glog.Info("doJob...")
    time.Sleep(time.Second) // 主线程执行任务

    glog.Info("service return: ", <-msg)
    wg.Wait()
}

程序运行结果:

从四条日志的打印时间来看,程序一共执行了大约1秒钟的时间,doJobservice start!几乎同时输出,两者在同时执行。过了一秒钟后,主函数执行完成,再从管道获取字符串数据,这样就省去了一次获取字符串数据的逻辑。假设获取字符串需要0.5秒,就节省了0.5秒的时间。

如果不使用这个异步的操作,那么整个函数的流程应该是:

  1. 主函数执行任务(1秒)
  2. 执行完成后再获取字符串数据(0.5秒)
  3. 函数结束(总耗时1.5秒)

使用异步之后,效率明显变高了。

三、有缓冲的管道

有缓冲的管道和无缓冲的管道最大的区别是往有缓冲管道内些数据时,如果管道还存在空间,写操作就不会阻塞,而无缓冲管道只要没有读端读出管道内数据,就会一直阻塞。

从上面的示例来看,msg是一个无缓冲区的管道,因此在创建的goroutine中写入数据后被阻塞,直到1秒后才打印出server done!。对这个协程而言,这1秒钟的过程无需阻塞,因为它的任务就是提取一个字符串消息然后放到管道,把数据放到管道后它的任务就完成了,可以直接结束了。没必要等到主函数读完才结束,否则很多协程都这样阻塞,纯粹浪费系统资源,降低性能。

那么这里就可以使用有缓冲区的管道来改进这一点,给管道设置一个缓冲区,新的协程往里面写数据就不会阻塞了,写完就能退出。代码中只需要修改msg创建时候的大小就可以了:

msg := make(chan string, 1)

执行结果:

可以看到,新协程启动和结束都是非常快速地,并没有等到一秒后才退出。同时主函数也在一秒后读到了字符串。