# 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
> ```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gocompiler.shizhz.me/2.-golang-bian-yi-qi-ci-fa-fen-xi/2.4-sao-miao-token.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
