> For the complete documentation index, see [llms.txt](https://gocompiler.shizhz.me/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gocompiler.shizhz.me/2.-golang-bian-yi-qi-ci-fa-fen-xi/2.4-sao-miao-token.md).

# 2.4 扫描Token

识别 Token 的逻辑位于如下源代码中：

### tokens.go&#x20;

该文件内定义了与 Token 相关的三种类型：

```go
type token uint    // Token 类型
type LitKind uint8 // 如果当前 Token 是字面量，则标识该字面量类型
type Operator uint // 如果当前 Token 是操作符，则标识操作符类型
```

然后初始化了4种枚举常量，除了 token， LitKind, Operator 三种类型对应的常量外，还初始化了操作符优先级的常量，这些常量都在扫描 Token 时使用。详情请查看 tokens.go 文件，为了节约篇幅，这里不再贴出所有代码。

### scanner.go&#x20;

所有扫描 Token 的逻辑都通过结构 `scanner` 实现，其定义如下：

```go
type scanner struct {
    source
    mode   uint
    nlsemi bool // if set '\n' and EOF translate to ';'

    // current token, valid after calling next()
    line, col uint
    blank     bool // line is blank up to col
    tok       token
    lit       string   // valid if tok is _Name, _Literal, or _Semi ("semicolon", "newline", or "EOF"); may be malformed if bad is true
    bad       bool     // valid if tok is _Literal, true if a syntax error occurred, lit may be malformed
    kind      LitKind  // valid if tok is _Literal
    op        Operator // valid if tok is _Operator, _AssignOp, or _IncOp
    prec      int      // valid if tok is _Operator, _AssignOp, or _IncOp
}
```

&#x20;重要的字段包括：

* source: 用来扫描字符
* tok: 当前 Token 的类型
* lit: 保存当前 Token 的字面量
* kind: 标识当前 lit 的种类
* op: 记录当前操作符
* prec: 标识当前操作符的优先级

扫描 Token 的逻辑在`next` 中，该方法首先忽略掉空白字符，然后通过当前字符并根据其内容解析出下一个 Token. 我们来详细看一下其如何解析出程序中的标识符的。

标识符（Identifier）就是程序中用于命名的那些词素，可以抽象的理解为用来绑定一个值的“变量”（先不要将此处的变量与程序中通过 `var` 申明的变量混淆，此处的变量是更加抽象的概念），先通过一个程序片段来理解一下：

```go
package gostudy

import fmtalias "fmt"

func main() {
    content := "hello, gopher!"
    fmtalias.Println(content)
}
```

其中用于命名的词素包括：

* gostudy: 为包命名
* fmtalias: 为所引入的包 fmt 命名
* main: 为函数命名
* content: 为局部变量命名
* Println: 绑定包中的函数值

这些标识符有时也被直接称为符号（Symbol），其 Token 类型被定义为`_Name`. Golang 将关键字也归为此类，在 init 方法中对关键字对应的 Token 类型做了初始化：

```go
func hash(s []byte) uint {
    return (uint(s[0])<<4 ^ uint(s[1]) + uint(len(s))) & uint(len(keywordMap)-1)
}

var keywordMap [1 << 6]token // size must be power of two

func init() {
    // populate keywordMap
    for tok := _Break; tok <= _Var; tok++ {
        h := hash([]byte(tok.String()))
        if keywordMap[h] != 0 {
            panic("imperfect hash")
        }
        keywordMap[h] = tok
    }
}
```

init 将所有关键字的 Token 类型存放在数组`keywordMap` 中，后续逻辑依此来判定扫描的标识符是否是关键字。

Golang 规范的标识符命名规则是：

* 名字由数字、英文字母、下划线、或者编码超过两个字节的 Unicode 字符（编码数字大于 utf8.RuneSelf）组成
* 不能以数字开头
* 大小写敏感
* 不能使用关键字
* 长度不限制

函数`next` 在过滤了空白字符后，首先就会判断当前字符是否是一个标识符的开端，其逻辑如下：

```go
func (s *scanner) next() {
    // 忽略开头代码
    if isLetter(s.ch) || s.ch >= utf8.RuneSelf && s.atIdentChar(true) {
        s.nextch()
        s.ident()
        return
    }
    // 忽略后续代码
} 
```

如果当前字符是合法的标识符开端，则通过 `scanner.ident()` 方法扫描对应标识符并返回。该方法也比较紧凑，我们可以贴出来看看：

```go
func (s *scanner) ident() {
    // accelerate common case (7bit ASCII)
    for isLetter(s.ch) || isDecimal(s.ch) {
        s.nextch()
    }

    // general case
    if s.ch >= utf8.RuneSelf {
        for s.atIdentChar(false) {
            s.nextch()
        }
    }

    // possibly a keyword
    lit := s.segment()
    if len(lit) >= 2 {
        if tok := keywordMap[hash(lit)]; tok != 0 && tokStrFast(tok) == string(lit) {
            s.nlsemi = contains(1<<_Break|1<<_Continue|1<<_Fallthrough|1<<_Return, tok)
            s.tok = tok
            return
        }
    }

    s.nlsemi = true
    s.lit = string(lit)
    s.tok = _Name
}
```

该方法的逻辑与 Golang 中对标识符的规范完全一致，其一直读取合法的标识符字符，再通过 `s.segment()` 取出标识符词素；然后根据之前初始化的 `keywordMap` 判断其是否是语言内置的关键字，如果是关键字则设置 tok 为对应的关键字类型，否则便将 tok 设置为 `_Name` 类型，并将词素存放在 lit 属性中。

> &#x20;nlsemi: Golang 中使用分号来作为语句结束的标识符，但是大多数情况下都可以忽略掉，忽略的规则见：<https://golang.org/ref/spec#Semicolons>. 该字段用来判断如果当前 token 是当前行的最后一个 token 的话，是否可以自动插入一个分号。根据规则可见，如果该 token 是关键字 break, continue, fallthrough 与 return 之一时，可以插入分号；而当该 token 是一个标识符时，总是可以插入分号。

我们来跑一下测试代码看看 scanner 对标识符的解析情况。在 `scanner_test.go` 中新增如下测试代码：

```go
func TestScanIdentTokens(t *testing.T) {
    code := `package gostudy

    import fmtalias "fmt"

    func main() {
        content := "hello, gopher!"
        fmtalias.Println(content)
    }
`

    var scn scanner

    scn.init(strings.NewReader(code), func(line, col uint, msg string) {
        fmt.Printf("%d:%d: %s\n", line, col, msg)
    }, 0)

    scn.next()
    for scn.tok != _EOF {
        if scn.tok == _Name {
            fmt.Printf("Token Type: %s, lit: %s\n", tokStrFast(scn.tok), scn.lit)
        }

        // 关键字标识符不会设置 lit 字段
        if scn.tok >= _Break && scn.tok <= _Var {
            fmt.Printf("Token Type: %s\n", tokStrFast(scn.tok))
        }
        scn.next()
    }
}

```

运行后可以得到结果：

> ```
> Token Type: package
> Token Type: name, lit: gostudy
> Token Type: import
> Token Type: name, lit: fmtalias
> Token Type: func
> Token Type: name, lit: main
> Token Type: name, lit: content
> Token Type: name, lit: fmtalias
> Token Type: name, lit: Println
> Token Type: name, lit: content
> ```
