[インデックス 1634] ファイルの概要
このコミットは、Go言語の初期開発段階における重要なマイルストーンの一つであり、Go Documentation Server (gds) の最初の実装を導入しています。これは、Goのソースコードから直接ドキュメントを生成し、Webブラウザを通じて提供するための基盤を築くものです。
コミット
commit 9acd2a973153cd98888429191c7c6ee5497efbab
Author: Robert Griesemer <gri@golang.org>
Date: Fri Feb 6 15:26:30 2009 -0800
snapshot:
- first stab at a Go Documentation Server (gds)
- various fixes to make initial version of gds work
R=r
OCL=24588
CL=24588
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9acd2a973153cd98888429191c7c6ee5497efbab
元コミット内容
このコミットの元の内容は、Go Documentation Server (gds) の最初の試作版と、その初期バージョンを動作させるための様々な修正です。具体的には、gds.go
という新しいファイルが追加され、Goのソースコードを解析し、その結果をHTML形式で出力するWebサーバー機能が実装されています。既存のMakefile
、compilation.go
、pretty.go
、printer.go
も、この新しいドキュメントサーバーの機能に対応するために変更されています。
変更の背景
Go言語は、その設計思想の一つとして「ドキュメンテーションの容易さ」を掲げています。コードとドキュメントを密接に連携させることで、開発者が常に最新かつ正確なドキュメントにアクセスできるようにすることを目指していました。このコミットは、その目標を達成するための初期ステップとして、Goのソースコードから直接ドキュメントを抽出し、Webブラウザで閲覧可能にするためのサーバーサイドのツールを開発する背景から生まれました。
当時のGo言語はまだ公開前の非常に初期段階にあり、開発者自身がコードベースを理解し、共有するためのツールが求められていました。gds
は、Goのコードを解析し、その構造(パッケージ、関数、変数など)をHTMLとして整形して提供することで、コードの可読性と探索性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連技術の初期段階における知識が役立ちます。
- Go言語の初期のツールチェイン: Go言語は、
6g
(コンパイラ)、6l
(リンカ) といったツールを使用してビルドされていました。これらは現在のgo build
コマンドに統合される前の、より低レベルなツールです。Makefile
の変更は、これらのツールを使ったビルドプロセスを反映しています。 go/parser
とgo/ast
パッケージの原型: Go言語のコンパイラは、ソースコードを解析して抽象構文木 (AST) を構築します。このコミットで変更されているcompilation.go
やprinter.go
は、Goのソースコードを読み込み、ASTを操作し、整形された出力(この場合はHTML)を生成する機能の初期実装に関連しています。net/http
パッケージの原型: Go言語の標準ライブラリには、Webサーバーを構築するための強力なnet/http
パッケージが含まれています。gds.go
では、このパッケージの初期のAPIを使用してHTTPサーバーを立ち上げ、リクエストを処理しています。text/tabwriter
パッケージの原型:printer.go
で使用されているtabwriter
は、テキストを整形してタブ区切りで出力するためのユーティリティです。このコミットでは、HTML出力のためにこのtabwriter
がどのように利用されているかが示されています。pretty
パッケージ: このコミットのファイルパスusr/gri/pretty/
が示すように、pretty
はGoのソースコードを整形(pretty-print)するためのパッケージです。gds
はこのpretty
パッケージの機能を拡張し、整形されたコードをHTMLとして提供します。
技術的詳細
このコミットの技術的な核心は、Goソースコードの解析、ASTの走査、そしてその結果をHTMLとしてレンダリングするプロセスにあります。
-
gds.go
の導入:- このファイルは、Go Documentation Server (gds) のエントリポイントです。
net/http
パッケージを使用してHTTPサーバーを起動し、特定のURLパス (/gds/
) へのリクエストを処理します。docServer
ハンドラ関数が、リクエストされたGoソースファイルのパスを解析し、Compilation.Compile
を呼び出してソースコードをコンパイル(この文脈では解析とAST構築)します。- コンパイルが成功すると、
Printer.Print
を呼び出して、解析されたプログラム構造をHTML形式でHTTPレスポンスとして書き出します。 - エラーが発生した場合は、HTTPステータス404 (Not Found) を返し、エラーメッセージを出力します。
flag
パッケージを使用して、サーバーのポート番号やGoのルートディレクトリを設定できるようにしています。
-
pretty.go
の変更:main
関数内で、コマンドライン引数からファイルパスを読み込み、Compilation.Compile
を呼び出してソースコードを解析します。Printer.Print
の呼び出しが変更され、os.Stdout
と新しいhtml
フラグを引数として渡すようになりました。これにより、pretty
コマンド自体もHTML出力をサポートできるようになります。flag.Bool("html", false, "generate html")
という新しいコマンドラインフラグが追加され、HTML出力の有効/無効を制御できるようになりました。
-
printer.go
の変更:Printer
構造体にhtml
フィールドが追加され、HTML出力モードであるかどうかの状態を保持するようになりました。Printer.Init
関数が変更され、html
フラグを引数として受け取るようになりました。htmlEscape
関数がPrinter
のメソッドとなり、P.html
フィールドを参照してHTMLエスケープを行うかどうかを決定します。これにより、HTMLエスケープ処理がPrinter
の状態に依存するようになります。TaggedString
、HtmlPrologue
、HtmlEpilogue
、HtmlIdentifier
などのHTML関連のメソッドが、P.html
フィールドに基づいて条件付きでHTMLタグを生成するように変更されました。- 特に注目すべきは、
HtmlPackageName
という新しいメソッドが追加された点です。これは、インポートされたパッケージ名に対して、http://localhost:6060/gds/src/lib/
のようなローカルのgdsサーバーへのリンクを生成する機能を持っています。これは、ドキュメントサーバーがGoのパッケージ間のナビゲーションを可能にするための初期の試みです。 DoIfStat
,DoForStat
,DoCaseClause
,DoSwitchStat
,DoSelectStat
などの制御構造の出力において、P.String(s.Pos, "if")
のような文字列リテラル出力からP.Token(s.Pos, Scanner.IF)
のようなトークンベースの出力に一部変更されています。これは、より汎用的な出力メカニズムへの移行を示唆しています。Print
関数のシグネチャが変更され、io.Writer
とhtml
フラグを引数として受け取るようになりました。これにより、Printer
が標準出力だけでなく、任意のio.Writer
(例: HTTPレスポンスライター)にHTMLまたはプレーンテキストを出力できるようになります。
-
compilation.go
の変更:ComputeDeps
関数内で、ソースファイル名から.go
拡張子をトリムするためにUtils.TrimExt
が使用されるようになりました。これは、ファイルパス処理の汎用性を高めるための小さな改善です。
-
Makefile
の変更:gds
ターゲットが追加され、gds.6
をビルドし、6l
リンカを使って実行可能ファイルgds
を生成するようになりました。all
ターゲットにgds
が追加され、デフォルトでgds
もビルドされるようになりました。gds.6
の依存関係としてutils.6 platform.6 compilation.6 printer.6
が明示され、gds
がこれらのコンポーネントに依存していることが示されています。
これらの変更は、Goのソースコードを解析し、その構造を理解し、それをWebブラウザで閲覧可能な形式で提供するという、Go Documentation Serverの基本的な機能を実現するためのものです。特に、printer.go
におけるHTML出力のロジックと、gds.go
におけるHTTPサーバーの統合がこのコミットの主要な技術的貢献です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
usr/gri/pretty/gds.go
(新規追加): Go Documentation ServerのメインロジックとHTTPハンドラが実装されています。usr/gri/pretty/pretty.go
:pretty
コマンドがHTML出力をサポートするためのフラグとPrinter.Print
の呼び出しが変更されています。usr/gri/pretty/printer.go
: HTML出力のロジックが追加・修正され、Printer
構造体がHTML出力の状態を持つようになりました。特にhtmlEscape
メソッドの変更とHtmlPackageName
の追加が重要です。usr/gri/pretty/Makefile
:gds
実行可能ファイルのビルドターゲットが追加されています。
コアとなるコードの解説
usr/gri/pretty/gds.go
// GDS: Go Documentation Server
package main
import (
"bufio";
"flag";
"fmt";
"http";
"io";
"net";
"os";
Utils "utils";
Platform "platform";
Compilation "compilation";
Printer "printer";
)
var urlPrefix = "/gds" // 6g BUG should be const
var (
verbose = flag.Bool("v", false, "verbose mode");
port = flag.String("port", "6060", "server port");
root = &Platform.GOROOT; // TODO cannot change root w/ passing it to printer
)
func getFilename(url string) string {
// strip URL prefix
if url[0 : len(urlPrefix)] != urlPrefix {
panic("server error - illegal URL prefix");
}
url = url[len(urlPrefix) : len(url)];
// sanitize source file name
return *root + Utils.TrimExt(url, ".go") + ".go";
}
func docServer(c *http.Conn, req *http.Request) {
if *verbose {
fmt.Printf("URL path = %s\n", req.Url.Path);
}
filename := getFilename(req.Url.Path);
var flags Compilation.Flags;
prog, nerrors := Compilation.Compile(filename, &flags);
if nerrors > 0 {
c.WriteHeader(http.StatusNotFound);
fmt.Fprintf(c, "compilation errors: %s\n", filename);
return;
}
c.SetHeader("content-type", "text/html; charset=utf-8");
Printer.Print(c, true, prog);
}
func main() {
flag.Parse();
if *verbose {
fmt.Printf("Go Documentation Server\n");
fmt.Printf("port = %s\n", *port);
fmt.Printf("root = %s\n", *root);
}
http.Handle(urlPrefix + "/", http.HandlerFunc(docServer));
err := http.ListenAndServe(":" + *port, nil);
if err != nil {
panic("ListenAndServe: ", err.String())
}
}
このファイルは、Go Documentation Serverの心臓部です。main
関数でHTTPサーバーを起動し、/gds/
プレフィックスを持つすべてのリクエストをdocServer
関数で処理するように設定しています。docServer
は、リクエストされたURLからファイル名を抽出し、Compilation.Compile
でGoソースファイルを解析します。成功した場合、Printer.Print
を呼び出して、解析結果をHTMLとしてクライアントに送信します。
usr/gri/pretty/pretty.go
--- a/usr/gri/pretty/pretty.go
+++ b/usr/gri/pretty/pretty.go
@@ -5,7 +5,8 @@
package main
import (
- Flag "flag";
+ "os";
+ "flag";
Platform "platform";
Printer "printer";
Compilation "compilation";
@@ -14,35 +15,36 @@ import (
var (
flags Compilation.Flags;
- silent = Flag.Bool("s", false, "silent mode: no pretty print output");
+ silent = flag.Bool("s", false, "silent mode: no pretty print output");
+ html = flag.Bool("html", false, "generate html");
)
func init() {
- Flag.BoolVar(&flags.Verbose, "v", false, "verbose mode: trace parsing");
- Flag.BoolVar(&flags.Sixg, "6g", true, "6g compatibility mode");
- Flag.BoolVar(&flags.Deps, "d", false, "print dependency information only");
- Flag.BoolVar(&flags.Columns, "columns", Platform.USER == "gri", "print column info in error messages");
- Flag.BoolVar(&flags.Testmode, "t", false, "test mode: interprets /* ERROR */ and /* SYNC */ comments");
+ flag.BoolVar(&flags.Verbose, "v", false, "verbose mode: trace parsing");
+ flag.BoolVar(&flags.Sixg, "6g", true, "6g compatibility mode");
+ flag.BoolVar(&flags.Deps, "d", false, "print dependency information only");
+ flag.BoolVar(&flags.Columns, "columns", Platform.USER == "gri", "print column info in error messages");
+ flag.BoolVar(&flags.Testmode, "t", false, "test mode: interprets /* ERROR */ and /* SYNC */ comments");
}
func usage() {
print("usage: pretty { flags } { files }\n");
- Flag.PrintDefaults();
+ flag.PrintDefaults();
sys.Exit(0);
}
func main() {
- Flag.Parse();
+ flag.Parse();
- if Flag.NFlag() == 0 && Flag.NArg() == 0 {
+ if flag.NFlag() == 0 && flag.NArg() == 0 {
usage();
}
// process files
- for i := 0; i < Flag.NArg(); i++ {
- src_file := Flag.Arg(i);
+ for i := 0; i < flag.NArg(); i++ {
+ src_file := flag.Arg(i);
if flags.Deps {
Compilation.ComputeDeps(src_file, &flags);
@@ -56,7 +58,7 @@ func main() {
if nerrors > 0 {
sys.Exit(1);
}
if !*silent && !flags.Testmode {
- Printer.Print(prog);
+ Printer.Print(os.Stdout, *html, prog);
}
}
}
pretty.go
では、flag
パッケージのインポート方法が変更され、html
という新しいブール型フラグが追加されました。これにより、pretty
コマンドを実行する際に-html
オプションを指定することで、HTML形式の出力を生成できるようになります。Printer.Print
の呼び出しも、os.Stdout
と新しいhtml
フラグを引数として渡すように変更されています。
usr/gri/pretty/printer.go
--- a/usr/gri/pretty/printer.go
+++ b/usr/gri/pretty/printer.go
@@ -28,7 +28,6 @@ var (
maxnewlines = flag.Int("maxnewlines", 3, "max. number of consecutive newlines");
// formatting control
- html = flag.Bool("html", false, "generate html");
comments = flag.Bool("comments", true, "print comments");
optsemicolons = flag.Bool("optsemicolons", false, "print optional semicolons");
)
@@ -79,6 +78,9 @@ const (
type Printer struct {
// output
text io.Write;
+
+ // formatting control
+ html bool;
// comments
comments *array.Array; // the list of all comments
@@ -118,9 +120,12 @@ func (P *Printer) NextComment() {
}
-func (P *Printer) Init(text io.Write, comments *array.Array) {
+func (P *Printer) Init(text io.Write, html bool, comments *array.Array) {
// writers
P.text = text;
+
+ // formatting control
+ P.html = html;
// comments
P.comments = comments;
@@ -137,8 +142,8 @@ func (P *Printer) Init(text io.Write, comments *array.Array) {
// ----------------------------------------------------------------------------
// Printing support
-func htmlEscape(s string) string {
- if *html {
+func (P *Printer) htmlEscape(s string) string {
+ if P.html {
\t\tvar esc string;
\t\tfor i := 0; i < len(s); i++ {\
\t\t\tswitch s[i] {
@@ -146,7 +151,7 @@ func htmlEscape(s string) string {
\t\t\tcase '&': esc = "&";
\t\t\tdefault: continue;
\t\t\t}
- \t\t\treturn s[0 : i] + esc + htmlEscape(s[i+1 : len(s)]);
+ \t\t\treturn s[0 : i] + esc + P.htmlEscape(s[i+1 : len(s)]);
\t\t}
\t}
\treturn s;
@@ -291,7 +296,7 @@ func (P *Printer) TaggedString(pos int, tag, s, endtag string) {
\t\t\t}
\t\t\t// calling untabify increases the change for idempotent output
\t\t\t// since tabs in comments are also interpreted by tabwriter
- \t\t\tP.Printf("%s", htmlEscape(untabify(ctext)));
+ \t\t\tP.Printf("%s", P.htmlEscape(untabify(ctext)));
\t\t\tif ctext[1] == '/' {
\t\t\t\t//-style comments must end in newline
@@ -337,7 +342,7 @@ func (P *Printer) TaggedString(pos int, tag, s, endtag string) {
\tif *debug {
\t\tP.Printf("[%d]", pos);
\t}
- \tP.Printf("%s%s%s", tag, htmlEscape(s), endtag);
+ \tP.Printf("%s%s%s", tag, P.htmlEscape(s), endtag);
\t// --------------------------------
\t// interpret state
@@ -368,6 +373,7 @@ func (P *Printer) String(pos int, s string) {
func (P *Printer) Token(pos int, tok int) {
P.String(pos, Scanner.TokenString(tok));
+ //P.TaggedString(pos, "<b>", Scanner.TokenString(tok), "</b>");
}
@@ -381,12 +387,12 @@ func (P *Printer) Error(pos int, tok int, msg string) {
// HTML support
func (P *Printer) HtmlPrologue(title string) {
-\tif *html {
+\tif P.html {
\t\tP.TaggedString(0,
\t\t\t"<html>\n"
\t\t\t"<head>\n"
\t\t\t"\t<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n"
-\t\t\t"\t<title>" + htmlEscape(title) + "</title>\n"
+\t\t\t"\t<title>" + P.htmlEscape(title) + "</title>\n"
\t\t\t"\t<style type=\"text/css\">\n"
\t\t\t"\t</style>\n"
\t\t\t"</head>\n"
@@ -399,7 +405,7 @@ func (P *Printer) HtmlPrologue(title string) {
func (P *Printer) HtmlEpilogue() {
-\tif *html {
+\tif P.html {
\t\tP.TaggedString(0,
\t\t\t"</pre>\n"
\t\t\t"</body>\n"
@@ -412,6 +418,17 @@ func (P *Printer) HtmlEpilogue() {
func (P *Printer) HtmlIdentifier(x *AST.Ident) {
obj := x.Obj;
-\tif *html && obj.Kind != SymbolTable.NONE {
+\tif P.html && obj.Kind != SymbolTable.NONE {
\t\t// depending on whether we have a declaration or use, generate different html
\t\t// - no need to htmlEscape ident
\t\tid := Utils.IntToString(obj.Id, 10);
@@ -429,6 +446,17 @@ func (P *Printer) HtmlIdentifier(x *AST.Ident) {
}
+func (P *Printer) HtmlPackageName(pos int, name string) {
+\tif P.html {
+\t\tsname := name[1 : len(name)-1]; // strip quotes TODO do this elsewhere eventually
+\t\t// TODO CAPITAL HACK BELOW FIX THIS
+\t\tP.TaggedString(pos, `<a href="http://localhost:6060/gds/src/lib/` + sname + `.go">`, sname, `</a>`);
+\t} else {
+\t\tP.String(pos, name);
+\t}\
+}
+
+
// ----------------------------------------------------------------------------
// Types
@@ -841,12 +858,12 @@ func (P *Printer) ControlClause(isForStat bool, init AST.Stat, expr AST.Expr, po
func (P *Printer) DoIfStat(s *AST.IfStat) {
-\tP.String(s.Pos, "if");
+\tP.Token(s.Pos, Scanner.IF);
\tP.ControlClause(false, s.Init, s.Cond, nil);
\tP.Block(s.Body, true);
\tif s.Else != nil {
\t\tP.separator = blank;
-\t\tP.String(0, "else");
+\t\tP.Token(0, Scanner.ELSE);
\t\tP.separator = blank;
\t\tP.Stat(s.Else);
\t}\
@@ -854,7 +871,7 @@ func (P *Printer) DoIfStat(s *AST.IfStat) {
func (P *Printer) DoForStat(s *AST.ForStat) {
-\tP.String(s.Pos, "for");
+\tP.Token(s.Pos, Scanner.FOR);
\tP.ControlClause(true, s.Init, s.Cond, s.Post);
\tP.Block(s.Body, true);
}\
@@ -862,11 +879,11 @@ func (P *Printer) DoForStat(s *AST.ForStat) {
func (P *Printer) DoCaseClause(s *AST.CaseClause) {
\tif s.Expr != nil {
-\t\tP.String(s.Pos, "case");
+\t\tP.Token(s.Pos, Scanner.CASE);
\t\tP.separator = blank;
\t\tP.Expr(s.Expr);
\t} else {
-\t\tP.String(s.Pos, "default");
+\t\tP.Token(s.Pos, Scanner.DEFAULT);
\t}\
\t// TODO: try to use P.Block instead
\t// P.Block(s.Body, true);
@@ -879,14 +896,14 @@ func (P *Printer) DoCaseClause(s *AST.CaseClause) {
func (P *Printer) DoSwitchStat(s *AST.SwitchStat) {
-\tP.String(s.Pos, "switch");
+\tP.Token(s.Pos, Scanner.SWITCH);
\tP.ControlClause(false, s.Init, s.Tag, nil);
\tP.Block(s.Body, false);
}\
func (P *Printer) DoSelectStat(s *AST.SelectStat) {
-\tP.String(s.Pos, "select");
+\tP.Token(s.Pos, Scanner.SELECT);
\tP.separator = blank;
\tP.Block(s.Body, false);
}\
@@ -940,7 +957,13 @@ func (P *Printer) Declaration(d *AST.Decl, parenthesized bool) {
\t\t\t\tP.String(d.Val.Pos(), ""); // flush pending ';' separator/newlines
\t\t\t}\
\t\t\tP.separator = tab;
-\t\t\tP.Expr(d.Val);
+\t\t\tif lit, is_lit := d.Val.(*AST.BasicLit); is_lit && lit.Tok == Scanner.STRING {
+\t\t\t\tP.HtmlPackageName(lit.Pos(), lit.Val);
+\t\t\t} else {
+\t\t\t\t// we should only reach here for strange imports
+\t\t\t\t// import "foo" "bar"
+\t\t\t\tP.Expr(d.Val);
+\t\t\t}\
\t\t\tP.separator = semicolon;
\t\tcase Scanner.TYPE:
@@ -1002,15 +1025,15 @@ func (P *Printer) Program(p *AST.Program) {
// ----------------------------------------------------------------------------
// External interface
-func Print(prog *AST.Program) {
+func Print(writer io.Write, html bool, prog *AST.Program) {
// setup
var P Printer;
padchar := byte(' ');
if *usetabs {
padchar = '\t';
}
-\ttext := tabwriter.New(os.Stdout, *tabwidth, 1, padchar, true, *html);\
-\tP.Init(text, prog.Comments);\
+\ttext := tabwriter.New(writer, *tabwidth, 1, padchar, true, html);\
+\tP.Init(text, html, prog.Comments);\
// TODO would be better to make the name of the src file be the title
P.HtmlPrologue("package " + prog.Ident.(*AST.Ident).Obj.Ident);
printer.go
は、Goコードの整形と出力のロジックをカプセル化しています。このコミットでは、Printer
構造体にhtml
フィールドが追加され、HTML出力モードを制御するようになりました。htmlEscape
メソッドは、このhtml
フィールドに基づいて文字列をエスケープするかどうかを決定します。
最も重要な変更は、HtmlPackageName
メソッドの追加です。これは、インポートパス(例: "fmt"
)を解析し、それをGo Documentation Server上の対応するパッケージのドキュメントへのハイパーリンクに変換します。これにより、生成されたHTMLドキュメント内でパッケージ間を簡単にナビゲートできるようになります。
また、Print
関数のシグネチャが変更され、io.Writer
とhtml
フラグを引数として受け取るようになりました。これにより、Printer
は標準出力だけでなく、HTTPレスポンスライターなど、任意の出力先にHTMLまたはプレーンテキストを書き出すことができるようになり、gds
との統合を可能にしています。
関連リンク
- Go言語の公式GitHubリポジトリ: https://github.com/golang/go
- Go言語のドキュメンテーション: https://go.dev/doc/ (現在のGoドキュメンテーションの姿)
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語の初期の設計に関する議論やメーリングリストのアーカイブ (当時の情報源): Go言語の初期の設計に関する情報は、Goの公式メーリングリストやデザインドキュメントに散見されますが、特定のURLを特定することは困難です。しかし、このコミットがGoのドキュメンテーションツールの基礎を築いたことは、その後の
go doc
コマンドやpkg.go.dev
といった公式ドキュメンテーションサイトの発展に繋がっています。