1. 程式人生 > 程式設計 >從詞法分析角度看 Go 程式碼的組成

從詞法分析角度看 Go 程式碼的組成

之前的 Go 筆記系列,已經完成到了開發環境搭建,原本接下來的計劃就是到語法部分了,但後來一直沒有前進。主要是因為當時的工作比較忙,分散了精力,於是就暫時放下了。

最近,準備重新把之前計劃撿起來。

第一步,肯定是瞭解 Go 基礎語法部分。原本計劃是寫 Go 編碼的一些基礎知識,但純粹聊什麼是關鍵字、識別符號、字面量、操作符實在有點無聊。

突然想到,詞法分析這塊知識還沒仔細研究過,那就從這個角度出發吧。通過逐步地拆解,將各個 token 進行歸類。

概述

我們知道,編譯型語言(比如 Go)的原始碼要經過編譯和連結才能轉化為計算機可以執行的程式,這個過程的第一步就是詞法分析。

什麼是詞法分析呢?

它就是將原始碼轉化為一個個預先定義的 token 的過程。為了便於理解,我們將其分為兩個階段進行介紹。

第一階段,對原始碼串進行掃描,按預先定義的 token 規則進行匹配並切分為一個個有語法含義、最小單元的字串,即詞素(lexme),並在此基礎上將其劃歸為某一類 token。這個階段,一些字元可能會被過濾掉,比如,空白符、註釋等。

第二階段,通過評估器 Evaluator 評估掃描出來的詞素,並確定它字面值,生成最終的 Token。

是不是有點不好理解呢?

如果之前從未接觸過這塊內容,可能沒有直觀感受。其實,看著很複雜,但的確非常簡單。

一個簡單的示例

先看一段程式碼,經典的 hello world,如下:

package main

import "fmt"

func main() {
    fmt.Println("Hello World"
) } 複製程式碼

我們可以通過這個例子的原始碼逐步拆解詞法分析的整個流程。

什麼是詞素

理論性的概念就不說了,直接看效果吧。

首先,將這段示例程式碼通過詞法分析的第一階段,我們將會得到如下內容:

package
main
\n
import
"fmt"
\n
func
main
(
)
{
\n
fmt
.
Println
(
"Hello World"
)
\n
}
複製程式碼

輸出的這一個個獨立的字元序列就是詞素。

詞素的切分規劃和語言的語法規則有關。此處的輸出中除了一些可見的字元,換行符同樣也具有語法含義,因為 Go 不像 C/C++ 必須是分號分隔語句,也可以通過換行符分隔。

原始碼分割為一個個詞素的過程是有一定的規則的,這和具體的語言有關。但雖有差異,其實規則都差不多,無非兩種,一是通過無語法含義的字元(空格符、製表符等)切分,還有是每個詞素可以用作為分隔符。

什麼是 token

token,也稱為詞法單元、記號等,它由名稱和字面值兩部分組成。從詞素到 token 有固定的對應關係,而且並非所有的 token 都有字面值。

將 hello world 的原始碼轉化為 token,我們將會得到如下的一張對應表格。

lexme name value
package PACKAGE "package"
main IDENT "main"
\n SEMICOLON "\n"
import IMPORT "import"
"fmt" STRING "\"fmt\""
\n SEMICOLON "\n"
func FUNC "func"
main IDENT "main"
( LPAREN ""
) RPAREN ""
{ LBRACE ""
fmt IDENT "fmt"
. PERIOD ""
Println IDENT "Println"
( LPAREN ""
"Hello World" STRING ""Hello World""
) RPAREN ""
\n SEMICOLON "\n"
} LBRACE ""
\n SEMICOLON "\n"

稍微有點長,因為這裡沒有省略。表格中的第一列是原始內容,第二列對應的 token 的名稱,最後一列是 token 的字面值。

從表格中可以觀察出,其中有一些 token 並沒有值,比如,括號、點,名稱本身已經表示了它們的內容。

token 的分類

token 一般可以分為關鍵字、識別符號、字面量、操作符這四個大類。這個分類其實在 Go 的原始碼中有非常明顯的體現。

檢視原始碼檔案 src/go/token/token.go,將會找到 Token 型別如下的幾個方法。

// 是否是字面常量
func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }
// 是否是操作符
func (tok Token) IsOperator() bool { return operator_beg < tok && tok < operator_end }
// 是否是關鍵字
func (tok Token) IsKeyword() bool { return keyword_beg < tok && tok < keyword_end }
複製程式碼

程式碼非常簡單,通過比較確定 Token 是否位於指定範圍確定它的型別。上面的這三個方法分別對應於判斷 Token 是字面常量、操作符還是關鍵字。

額?怎麼沒有識別符號呢?

當然也有啦,只不過它不是 Token 的方法,而是單獨的一個函式。如下:

func IsIdentifier(name string) bool {
	for i,c := range name {
		if !unicode.IsLetter(c) && c != '_' && (i == 0 || !unicode.IsDigit(c)) {
			return false
		}
	}
	return name != "" && !IsKeyword(name)
}
複製程式碼

我們常說的變數、常量、函式、方法的名稱不能為關鍵字,且必須是由字母、下劃線或數字組成,且名稱的開頭不能為數字的規則,看到這個函式是不是一些就明白了。

到這裡,其實已經寫的差不多了。但想想還是拿其中一個型別再簡單說說吧。

關鍵字

就以關鍵字為例吧,Go 中的關鍵字有哪些呢?

繼續看原始碼。將之前那段如何判斷一個 token 是關鍵字的程式碼再看一遍。如下:

func (tok Token) IsKeyword() bool {
	return keyword_beg < tok && tok < keyword_end
}
複製程式碼

只要 Token 大於 keyword_beg 且小於 keyword_end 即為關鍵字,看起來還挺好理解的。那在 keyword_begkeyword_end 之間有哪些關鍵字呢?程式碼如下:

const (
	...
	keyword_beg
	// Keywords
	BREAK
	CASE
	CHAN
	CONST
	CONTINUE

	...

	SELECT
	STRUCT
	SWITCH
	TYPE
	VAR
	keyword_end
	...
)
複製程式碼

總共梳理出了 25 個關鍵字。如下:

break       case        chan    const       continue
default     defer       else    fallthrough for
func        go          goto    if          import
interface   map         package range       return
select      struct      switch  type        var
複製程式碼

關鍵字的確挺少的。可見。。。

嗯?!

是不是猜到我要說,Go 語言就是簡潔,關鍵字的都這麼少。你看 Java,足足有 53 個關鍵字,其中有兩個是保留字。你再看看 Go,連保留字都沒有,就是這麼自信。

既然你猜到了,那我還是先不說了吧。

其他

操作符和字面常量就不追了,思路都是一樣的。

Go 中的操作符有 47 個,比如賦值運運算元、位運運算元、算術運運算元,比較運運算元,還有其他的操作符。相信我吧,都是從原始碼中數出來的,沒有看任何資料。[此處應該放個捂臉笑]。

字面常量呢?

有 5 種型別,分別是 INT(整型)、FLOAT(浮點型)、IMG(複數型別)、CHAR(字元型)、STRING(字串型)。

總結

文章寫完了,前面扯了那麼一堆廢話,其實就只是為了介紹 Go 語法中用到的關鍵字、識別符號、運運算元、字面量從哪裡找。並且,最終它們如何使用也沒有怎麼說明。

純粹為了好玩嗎?當然不是(是)。因為。。。,先不劇透了,避免後面尷尬。

閱讀資料

Go 程式是怎麼跑起來的

go-lexer 詞法分析

Lexical analysis

詞法分析