huanglei

long long ago

编译器 - 词法分析

定义

词法分析器的功能:输入源程序,按照构词规则分解成一系列单词符号。单词是语言中具有独立意义的最小单位,包括关键字、标识符、运算符、界符和常量等

  • 关键字 是由程序语言定义的具有固定意义的标识符。例如,Java 中的if,while,for都是保留字
  • 标识符 用来表示各种名字,如变量名,数组名,过程名等等
  • 常数 常数的类型一般有整型、实型、布尔型、文字型等
  • 运算符 如+、-、*、/等等
  • 界符 如逗号、分号、括号、等等

理论的知识还是比较复杂的,有状态转换图(FA)、正规式与正规集等,我的编译器系列文章是从开发人员的角度倒着来学习,先把算法过程捋一遍,然后再回过头去看理论知识,这样更容易理解理论说的是什么。

思路

这边的算法是从go中text/scanner包中的Scanner移植过来的,去掉了unicode的支持,做了一点简化。
我们主要是为了理解整个过程,所以有些严禁的东西都去掉了。

一. 简介

  • 整个算法用了两个缓存区(源数据缓冲区和词法单元临时缓冲区)
  • 三个指针,分别指向当前源数据缓存区位置和词法单元的开始位置及结束位置
  • 当前读取的字符的变量
  • 当前读取的行和列变量
  • 判断数字、字符、标识符的方法
  • 处理数字、字符串、标识符、注释的方法

二. 文法

  1. 字符

    正则表达式: a-z | A-Z
  2. 数字

    正则表达式: 0-9
  3. 标识符

    以字符(a-zA-Z)或下划线(_)开头,后面只包含任意个字符、数字、下划线(a-zA-Z0-9_)的字符串
    正则表达式: [a-zA-Z_]+[a-zA-Z0-9_]*
  4. 数值

    • 整数或浮点数

      以数字(0-9)开头,后面只包含一个小数点(.)和其他数字(0-9)的字符串
      正则表达式: [0-9]+\.?[0-9]*
    • 浮点数

      以小数点(.)开头,后面只包含其他数字的字符串
      正则表达式: \.[0-9]+
  5. 字符串

    • 单引号
      以单引号(')开头,直到找到下一个单引号结束
      正则表达式: '.*'
    • 双引号
      以双引号(")开头,直到找到下一个双引号结束
      正则表达式: ".*"
  6. 注释

    • 单行注释

      以双反斜杠(//)开头,直到当前行结束
      正则表达式: //.*
    • 多行注释或局部注释

      以反斜杠(/)和星号(*)开头,直到找到下一个反斜杠和星号结束
      正则表达式: /\*.*\*/

三. 算法

  1. 大体思路
    首先获取整个字符流实例,读取到源数据缓存中,然后从源数据缓存中读取一个字符,判断是否符合文法规则和是否继续读取下一个字符,还是退出当前规则,重新开始。反复以上的过程直到字符流中的数据读完,算法很简单。

  2. 详细过程
    先用文字给说明一下,再对照代码就明白了

    定义变量: 
    字符流实例
    //字符数组,长度为定义长度+1,为什么要+1后面解释
    //这里我们默认长度可以是1024,那数组的长度就是1024+1
    //当然也可以是其他长度2048+1等
    源数据缓存
    源数据缓存指针
    //动态字符数组,不确定长度
    词法单元暂存缓存
    词法单元指针开始,词法单元指针结束
    当前行,列
    当前读取的字符

    初始化:
    1. 打开文件或字符串到字符流实例
    //读取字符的时候如果读到0x80表示缓冲区没有可读字符或已经读完
    //需要从字符流中读取未读的字符
    2. 源数据缓存第一个字符为0x80
    3. 源数据缓存指针为0
    //只要有数据开始行肯定是1,当前列为0
    4. 当前行为1
    5. 当前列为0
    //默认没有读取任何字符,用-2来标记,后面用来判断
    6. 当前读取的字符为-2
    //词法单元暂存缓存也没有数据,指针开始置位-1,结束位置为0
    7. 词法单元指针开始为-1
    8. 词法单元指针结束0

    读取字符方法:
    1. 根据源数据缓存指针从源数据缓存中读取一个字符
    2. 如果读取的字符是0x80
    //这边有种情况
    //如果前面的处理正好符合某个规则,但是缓存中由于
    //长度的关系没有把完整的字符流中的数据读取进来,
    //所以必须先把之前处理的正确数据存起来
    //这也是这个词法单元暂存缓存的作用
    3. 如果词法单元指针开始大于等于0
    4. 把源数据缓存中词法单元指针开始到当前源数据缓存指针的数据存到词法单元暂存缓存
    //后面要重新读,长度位置都会变,所以重置
    5. 重置词法单元指针为0
    //表示还没有从字符流读取数据或源数据缓存已经读完
    6. 读取最多源数据缓存定义长度(1024)个字符到源数据缓存
    //因为初始化的时候预留的1个字节
    //所以即使读取的数据长度为1024我们也可以在1025的位置定义标记0x80
    //为什么要标记0x80原因上面解释
    //如果读取的长度小于1024则只要在读取数据的后面加上0x80
    7. 源数据缓存中数据的后面一个字节设置为0x80
    //重新读取过数据所以源数据缓存指针必须重置
    8. 重置源数据缓存指针为0
    9. 如果字符流读取错误
    10. 如果数据读完出错返回EOF
    11. 否则输出错误信息
    12. 重新根据源数据缓存指针从源数据缓存的位置读取一个字符,也就是第0个字符
    //当前位置已经读过,指针后移,当前列增加
    13. 缓存指针的位置+1
    14. 当前列+1
    15. 如果读取的字符为换行
    //表示新的一行开始
    16. 当前行+1
    17. 当前列0
    18. 返回读取的字符

    开始扫描:
    1. 获取当前读取的字符
    //重新扫描开始,词法单元指针为-1
    //保证在调用(读取字符方法)不会从词法单元暂存缓存获取数据
    2. 重置词法单元指针开始为-1
    3. 如果获取的字符是-2
    //表示刚开始读
    4. 调用(读取字符方法)重新获取
    5. 如果获取的字符是制表符(\t)、回车(\r)、换行(\n)、空字符串(' ')
    6. 调用(读取字符方法)重新获取,直到不满足规则
    //读到了字符,因为获取字符以后指针往后移了,那当前字符的位置就要-1
    //最后我们会根据开始位置和结束位置来获取当前获取的词法单元内容
    7. 重置词法单元指针开始为源数据缓存指针-1
    8. 清空词法单元暂存缓存
    //因为有可能我们分析出来的字符在我们的规则中不存在
    //但是也必须要返回,所以这里暂时先这么设置
    9. 定义词法单元类型为获取的获取的字符
    10. 判断获取的字符
    11. 如果是EOF
    //源数据缓存已经处理完,同时字符流也没有数据,直接返回
    12. 返回
    13. 如果是字符
    14. 词法单元类型为标识符类型
    15. 调用(标识符处理方法)开始处理
    16. 更新获取的字符为返回处理完后返回的字符
    17. 如果是数字
    18. 调用(数值处理方法)开始处理
    19. 更新获取的字符为返回处理完后返回的字符
    20. 根据返回更新词法单元类型为整型或浮点型
    21. 如果是单引号(‘)或双引号(“)
    22. 词法单元类型为字符串类型
    23. 字符串处理方法开始处理
    24. 调用(读取字符方法)再次获取
    25. 如果是小数点(.)
    //因为不确定后面是不是数字,所以再获取一个
    26. 调用(读取字符方法)再次获取
    27. 如果是数字
    //因为有小数点一定是浮点数
    28. 词法单元类型为浮点型
    29. 调用(浮点数处理方法)开始处理
    30. 更新获取的字符为返回处理完后返回的字符
    31. 如果是反斜杠(/)
    // 因为不确定是不是注释,所以再获取一个
    32. 调用(读取字符方法)再次获取
    33. 如果是/或*
    34. 词法单元类型为注释
    35. 调用(注释处理方法)开始处理
    36. 更新获取的字符为返回处理完后返回的字符
    37. 如果都没有匹配到
    38. 调用(读取字符方法)更新获取的字符
    //原因通词法单元指针开始
    39. 词法单元指针结束为源数据缓存指针-1
    40. 更新当前读取的字符为获取的字符
    41. 返回词法单元类型

    文法处理方法:这边就不一一写出来了,后面代码中会有,比较简单

图解

代码

代码用的是GO

  1. 代码

    package lexer

    import (
    "bytes"
    "fmt"
    "io"
    "os"
    )

    // 缓存长度
    const bufLen = 1024

    // 标识符
    const readFlag = 0x80

    // 词法单元类型
    const (
    EOF = 1 << iota
    Ident
    Int
    Float
    String
    Comment
    )

    var tokenString = map[rune]string{
    EOF: "EOF",
    Ident: "Ident",
    Int: "Int",
    Float: "Float",
    String: "String",
    Comment: "Comment",
    }

    // TokenString 词法单元类型
    func TokenString(token rune) string {
    if s, found := tokenString[token]; found {
    return s
    }
    return fmt.Sprintf("%q", string(token))
    }

    // Lexer 词法分析器
    type Lexer struct {

    // 数据源
    src io.Reader
    // 数据缓存区(bufLen字符),多一位为了下一次读取判断
    srcBuf [bufLen + 1]byte
    // 当前读取位置
    srcPos int

    // 词法临时缓存区
    tokenBuf bytes.Buffer
    // 词法单元开始位置
    tokenPos int
    // 词法单元结束位置
    tokenEnd int

    // 当前读取的字符
    ch rune

    // 当前行
    line int
    // 当前列
    column int

    Filename string
    }


    // 判断字符
    func isLetter(ch rune) bool {
    return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
    }

    // 判断数字
    func isDecimal(ch rune) bool {
    return ch >= '0' && ch <= '9'
    }

    // 判断标识符
    func isIdentRune(ch rune, i int) bool {
    return ch == '_' || isLetter(ch) || (isDecimal(ch) && i > 0)
    }

    // 扫描标识符
    func (lexer *Lexer) scanIdentifier() rune {
    ch := lexer.next()
    for i := 1; isIdentRune(ch, i); i++ {
    ch = lexer.next()
    }
    return ch
    }

    // 扫描字符串
    func (lexer *Lexer) scanString(quote rune) {
    ch := lexer.next()
    for ch != quote {
    if ch == '\n' || ch < 0 {
    lexer.error("literal not terminated")
    return
    }
    ch = lexer.next()
    }
    }


    // 扫描数字
    func (lexer *Lexer) scanNumber(ch rune, seenDot bool) (rune, rune) {
    var tk rune
    if !seenDot {
    tk = Int
    for isDecimal(ch) {
    ch = lexer.next()
    }
    if ch == '.' {
    ch = lexer.next()
    seenDot = true
    }
    }

    // 处理小数部分
    if seenDot {
    tk = Float
    for isDecimal(ch) {
    ch = lexer.next()
    }
    }
    return tk, ch
    }

    // 扫描注释
    func (lexer *Lexer) scanComment(ch rune) rune {
    if ch == '/' {
    // 单行注释
    ch = lexer.next()
    for ch != '\n' && ch >= 0 {
    ch = lexer.next()
    }
    return ch
    }

    // 非单行注释
    ch = lexer.next()
    for {
    if ch < 0 {
    lexer.error("comment not terminated")
    break
    }
    ch0 := ch
    ch = lexer.next()
    if ch0 == '*' && ch == '/' {
    ch = lexer.next()
    break
    }
    }
    return ch
    }

    func (lexer *Lexer) peek() rune {
    if lexer.ch == -2 {
    lexer.ch = lexer.next()
    if lexer.ch == '\uFEFF' {
    lexer.ch = lexer.next() // ignore BOM
    }
    }
    return lexer.ch
    }

    // 读取下一个字符
    func (lexer *Lexer) next() rune {
    ch := rune(lexer.srcBuf[lexer.srcPos])
    // 如果是读取标记,则进行读取
    if ch >= readFlag {

    // 字符处理一半,缓存区已满,使用词法缓存暂存
    if lexer.tokenPos >= 0 {
    lexer.tokenBuf.Write(
    lexer.srcBuf[lexer.tokenPos:lexer.srcPos]
    )
    lexer.tokenPos = 0
    }

    // 从缓冲区中继续读取数据
    n, err := lexer.src.Read(lexer.srcBuf[0:bufLen])
    // 最后一位设置为下一次继续读取的标记
    lexer.srcBuf[n] = readFlag
    // 重置当前读取位置
    lexer.srcPos = 0
    if err != nil {
    // 其他错误则打印
    if err != io.EOF {
    lexer.error(err.Error())
    }
    // 如果已经读完返回End Of File
    return EOF
    }
    // 重新读取第一次字符
    ch = rune(lexer.srcBuf[lexer.srcPos])
    }
    lexer.srcPos++
    lexer.column++
    switch {
    case ch == '\n':
    lexer.line++
    lexer.column = 0
    }
    return ch
    }

    // 错误
    func (lexer *Lexer) error(msg string) {
    fmt.Fprintf(os.Stderr, "%s: %s\n", lexer.Position(), msg)
    }

    // Init 初始化
    func (lexer *Lexer) Init(src io.Reader) {
    lexer.src = src

    lexer.line = 1
    lexer.column = 0

    lexer.srcBuf[0] = readFlag
    lexer.srcPos = 0

    lexer.tokenPos = -1
    lexer.tokenEnd = 0

    lexer.ch = -2

    lexer.Filename = "nofile"
    }

    // Scan 扫描
    func (lexer *Lexer) Scan() rune {

    // 获取字符
    ch := lexer.peek()

    lexer.tokenPos = -1

    // 忽略 制表符 回车换行 空字符串
    for ch == '\t' || ch == '\n' || ch == '\r' || ch == ' ' {
    ch = lexer.next()
    }

    // 清空词法缓存
    lexer.tokenBuf.Reset()

    // 设置词法单元开始位置,因为next()中pos++,
    // 所以开始位置在缓存区中的位置往前移一格
    lexer.tokenPos = lexer.srcPos - 1

    tk := ch
    switch {
    case isIdentRune(ch, 0):
    // 标识符
    tk = Ident
    ch = lexer.scanIdentifier()
    case isDecimal(ch):
    // 数值型,要判断是整数还是浮点数
    tk, ch = lexer.scanNumber(ch, false)
    default:
    switch ch {
    case EOF:
    break
    case '"', '\'':
    lexer.scanString(ch)
    tk = String
    ch = lexer.next()
    case '/':
    ch = lexer.next()
    if ch == '/' || ch == '*' {
    ch = lexer.scanComment(ch)
    tk = Comment
    }
    case '.':
    ch = lexer.next()
    if isDecimal(ch) {
    tk, ch = lexer.scanNumber(ch, true)
    }
    default:
    ch = lexer.next()
    }
    }

    // 同样多读取了一个,所以把词法单位结束位置在缓存区中的位置往前移一格
    lexer.tokenEnd = lexer.srcPos - 1
    lexer.ch = ch
    return tk
    }

    // Position 当前文件位置
    func (lexer *Lexer) Position() string {
    var line int
    if lexer.column > 0 {
    line = lexer.line
    } else {
    // 如果读到\n,因为读取的时候已经加1了,所以这边减去1
    line = lexer.line - 1
    }
    return fmt.Sprintf("%s:%d:%d", lexer.Filename, line, lexer.column)
    }

    // Token 词法单元
    func (lexer *Lexer) Token() string {
    if lexer.tokenPos < 0 {
    return ""
    }

    if lexer.tokenEnd < lexer.tokenPos {
    lexer.tokenEnd = lexer.tokenPos
    }

    // 词法单元缓存区为空,直接返回当前词法开始位置和结束位置的字符
    if lexer.tokenBuf.Len() == 0 {
    return string(lexer.srcBuf[lexer.tokenPos:lexer.tokenEnd])
    }

    // 把后面读取的缓冲区中的数据加入到词法单元的缓存区形成完整的字符
    lexer.tokenBuf.Write(lexer.srcBuf[lexer.tokenPos:lexer.tokenEnd])
    return lexer.tokenBuf.String()
    }

    代码基本和上面的文字描述一致

  2. 测试


    func Test1() {
    const src = `
    // Comment begins at column 5.

    This line should not be included in the output.

    /*
    This multiline comment
    should be extracted in
    its entirety.
    */
    `

    var lexer Lexer
    lexer.Init(strings.NewReader(src))
    for tok := lexer.Scan(); tok != EOF; tok = lexer.Scan() {
    fmt.Printf("%s: %s %s\n",
    lexer.Position(), lexer.Token(), TokenString(tok)
    )
    }

    // nofile:2:0: // Comment begins at column 5. Comment
    // nofile:4:6: This Ident
    // nofile:4:11: line Ident
    // nofile:4:18: should Ident
    // nofile:4:22: not Ident
    // nofile:4:25: be Ident
    // nofile:4:34: included Ident
    // nofile:4:37: in Ident
    // nofile:4:41: the Ident
    // nofile:4:48: output Ident
    // nofile:4:0: . "."
    // nofile:10:0: /*
    // This multiline comment
    // should be extracted in
    // its entirety.
    // */ Comment
    //

再想想

Openvpn 内网互通

先说一个具体的场景:
上海内网网段: 192.168.10.0/24
北京内网网段: 192.168.1.0/24
现在要求上海和北京能够互联互通。
常规方法就是整个 vpn(vpn 的安装和基本配置可以看我的另外一篇文章),那具体这么弄?
我们一步步来看

第一种方式

我们把 vpn 服务安装在上海或北京的服务器上,我们这里拿上海做例子

  • 前提条件

上海必须有一个公网固定 IP,比如 123.123.123.123

  • vpn 安装及配置

我们先把 vpn 服务安装到上海的某台服务器上(安装过程可以查找我的另外一篇文章)

然后修改配置文件,注释删掉后大概是这个样子

;local a.b.c.d # 如果有多网卡可以指定一个

port 1194 # 端口号
proto tcp # 协议
dev tun # 路由模式

# 证书位置
ca /etc/openvpn/server/certs/ca.crt
cert /etc/openvpn/server/certs/server.crt
key /etc/openvpn/server/certs/server.key
dh /etc/openvpn/server/certs/dh.pem

# 客户端地址池
server 10.8.0.0 255.255.255.0
ifconfig-pool-persist /etc/openvpn/ipp.txt

# 客户端添加一个路由
push "route 192.168.10.0 255.255.255.0"

client-config-dir /etc/openvpn/ccd
client-to-client
keepalive 10 120
cipher AES-256-CBC
comp-lzo
user openvpn
group openvpn
persist-key
persist-tun
status openvpn-status.log
log-append openvpn.log
comp-lzo
verb 3
mute 20

这里要解释一下

# 客户端添加一个路由
push "route 192.168.10.0 255.255.255.0"

意思就是:vpn客户端只要访问 192.168.10.0/24 的 IP,从我们 vpn 拨号的这个网口发出去

试想一下北京某台电脑 IP 为 192.168.1.6 的电脑,访问 192.168.10.6 的时候,是不是就能够通过 vpn 的网口发出去了?

vpn 的配置暂时说到这,后面还要修改。

  • 防火墙(iptables)

vpn配置的地址池是10.8.0.0/24,客户端的IP地址必然为10.8.0.x,那接收到的请求源IP地址必然也是10.8.0.x,那在北京我们要访问的IP是192.168.10.6,这个IP和我们的vpn不在同一个网段,如何通呢?

我们先说说这个访问的流程,北京访问192.168.10.6,因为加了路由,则请求会从vpn客户端的网口发送到默认网关,vpn服务器会接受这个请求,然后查找192.168.10.6这个IP地址,在vpn的网段中因为没有这个IP,所以访问不了?

那如果我们把这个请求转发出去是不是就可以通了呢?

我们先看看vpn服务器的网卡信息

ip a

我们可以发现会有一个物理网卡(可能是ethX或ensX)的信息和一个tun的虚拟网卡信息,物理网卡正好是我们192.168.10.0/24的某一个IP,我们只要把从tun来的请求转到物理网卡,那就可以通啦。

我们用iptables在SNAT(源地址目标转换)做一个规则,应该就可以,命令如下

sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o [ethX|ensX] -j MASQUERADE

# 查看
sudo iptables -nL -t nat

解释:在防火墙的nat表当中的POSTROUTING链上添加(-A)一条规则,规则是从(-s)10.8.0.0网段过来的请求,出去(-o OPUTPUT)的时候都走ethX|ensX

另外服务器要支持IP路由转发,系统配置要修改一下

vi /etc/sysctl.conf

net.ipv4.ip_forward = 1 # 没有则添加,有修改为1(0禁止,1开启)

sysctl -p

OK! 这回再从北京的那台电脑上

ping 192.168.10.6

是不是通了?

Openvpn 配置

注意:本教程在 centos7 下操作

依赖软件

  • 软件版本
openvpn 2.4.9
easy-rsa 3.0.7

如果版本不一致,后面的路径会有所差别
  • 安装扩展源
yum install epel-release -y
  • 安装 openvpn easy-rsa3
yum install openvpn easy-rsa -y

服务器配置

  • 拷贝配置文件
cp /usr/share/doc/openvpn-2.4.9/sample/sample-config-files/server.conf  /etc/openvpn/server/

这里的openvpn版本如果不一致,请注意路径
cp -r /usr/share/easy-rsa /etc/openvpn/
cp /usr/share/doc/easy-rsa-3.0.7/vars.example /etc/openvpn/easy-rsa/3/vars

这里的easy-rsa版本如果不一致,请注意路径
  • 进入 easy-rsa 目录
cd /etc/openvpn/easy-rsa/3
  • 创建空 PKI
./easyrsa init-pki
  • 创建 CA 证书(不使用密码)
./easyrsa build-ca nopass
(一路回车就可以)
  • 创建服务端证书(不使用密码)
./easyrsa gen-req server nopass
(一路回车就可以)
  • 签约服务端证书
./easyrsa sign server server
  • 创建 Diffie-Hellman
./easyrsa gen-dh
  • 整理服务器证书
mkdir /etc/openvpn/server/certs
cp /etc/openvpn/easy-rsa/3/pki/dh.pem /etc/openvpn/server/certs
cp /etc/openvpn/easy-rsa/3/pki/ca.crt /etc/openvpn/server/certs
cp /etc/openvpn/easy-rsa/3/pki/issued/server.crt /etc/openvpn/server/certs
cp /etc/openvpn/easy-rsa/3/pki/private/server.key /etc/openvpn/server/certs
  • 服务器配置文件
vim /etc/openvpn/server/server.conf

需修改项如下:

local 192.168.10.155 # 当前服务器ip地址,请修改

proto tcp # 去掉前面的;
;proto udp # 前面加上;

ca /etc/openvpn/server/certs/ca.crt # 修改路径
cert /etc/openvpn/server/certs/server.crt #
key /etc/openvpn/server/certs/server.key #

dh /etc/openvpn/server/certs/dh.pem #

ifconfig-pool-persist /etc/openvpn/ipp.txt #

client-to-client # 去掉前面的;

comp-lzo # 去掉前面的;

mute 20 # 去掉前面的;

log-append openvpn.log # 去掉前面的;

user openvpn # 去掉前面的;修改运行用户
group openvpn # 去掉前面的;修改运行用户组

;explicit-exit-notify 1 # 前面加上;

;tls-auth ta.key 0 # 前面加上;
  • 启动 openvpn 服务
systemctl start openvpn-server@server.service
查看启动状态
systemctl status openvpn-server@server.service
添加到自启动
systemctl enable openvpn-server@server.service

客户端配置

  • 客户端配置文件
cp /usr/share/doc/openvpn-2.4.9/sample/sample-config-files/client.conf /etc/openvpn/client/
  • 修改配置文件
# 没有用的都可以删除了,留以下内容

client

dev tun

proto tcp
remote 192.168.10.155 1194 # 服务器ip地址和端口号,和服务端配置一致
resolv-retry infinite
nobind
persist-key
persist-tun

ca ca.crt # 修改配置文件路径
cert client.crt #
key client.key #

remote-cert-tls server
cipher AES-256-CBC
comp-lzo
verb 3
mute 20
  • 创建客户证书(不使用密码)
创建test用户

./easyrsa gen-req test nopass
(一路回车就可以)
  • 签约客户证书
./easyrsa import-req /etc/openvpn/easy-rsa/3/pki/reqs/test.req test
./easyrsa sign client test
  • 安装 openvpn client

下载地址

安装好后导入配置文件

Centos配置

Centos 安装好后初始化

  • 安装第三方源
yum install epel-release -y
  • 安装时间同步工具
yum install ntpdate -y
ntpdate cn.ntp.org.cn

Golang配置

首先下载编译器,因为要翻墙,所以找个国内的地址。 根据你的操作系统,选择最新的一个版本,直接下载一个免安装的包 下载,下载完后解压到某个目录

  • mac系统:gox.x.x.darwin-amd64.tar.gz
  • linux系统:gox.x.x.linux-arm64.tar.gz
  • window系统:gox.x.x.windows-amd64.zip

Mac && Linux

配置环境变量
vim ~/.bash_profile

# 设置GOROOT 
export GOROOT=/当前go解压的目录
# 设置GOPATH (新版本已不用设置,由于博主是从老版本过来的所以保留之前的)
export GOPATH=/当前go工作的目录
# 因为google被禁了所以要设置代理
export GOPROXY=https://goproxy.io
# 使用go mod
export GO111MODULE=on
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

Window

配置环境变量

我的电脑->属性->环境变量->添加

GOROOT: /当前go解压的目录

GOPATH: /当前go工作的目录

GOPROXY: https://goproxy.io

GO111MODULE: on

PATH: 最后添加;%GOROOT%/bin:%GOPATH%/bin
go env

Proudly powered by Hexo and Theme by Hacker
© 2020 huanglei