Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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サーバー機能が実装されています。既存のMakefilecompilation.gopretty.goprinter.goも、この新しいドキュメントサーバーの機能に対応するために変更されています。

変更の背景

Go言語は、その設計思想の一つとして「ドキュメンテーションの容易さ」を掲げています。コードとドキュメントを密接に連携させることで、開発者が常に最新かつ正確なドキュメントにアクセスできるようにすることを目指していました。このコミットは、その目標を達成するための初期ステップとして、Goのソースコードから直接ドキュメントを抽出し、Webブラウザで閲覧可能にするためのサーバーサイドのツールを開発する背景から生まれました。

当時のGo言語はまだ公開前の非常に初期段階にあり、開発者自身がコードベースを理解し、共有するためのツールが求められていました。gdsは、Goのコードを解析し、その構造(パッケージ、関数、変数など)をHTMLとして整形して提供することで、コードの可読性と探索性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語および関連技術の初期段階における知識が役立ちます。

  • Go言語の初期のツールチェイン: Go言語は、6g (コンパイラ)、6l (リンカ) といったツールを使用してビルドされていました。これらは現在のgo buildコマンドに統合される前の、より低レベルなツールです。Makefileの変更は、これらのツールを使ったビルドプロセスを反映しています。
  • go/parsergo/astパッケージの原型: Go言語のコンパイラは、ソースコードを解析して抽象構文木 (AST) を構築します。このコミットで変更されているcompilation.goprinter.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としてレンダリングするプロセスにあります。

  1. 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のルートディレクトリを設定できるようにしています。
  2. pretty.goの変更:

    • main関数内で、コマンドライン引数からファイルパスを読み込み、Compilation.Compileを呼び出してソースコードを解析します。
    • Printer.Printの呼び出しが変更され、os.Stdoutと新しいhtmlフラグを引数として渡すようになりました。これにより、prettyコマンド自体もHTML出力をサポートできるようになります。
    • flag.Bool("html", false, "generate html")という新しいコマンドラインフラグが追加され、HTML出力の有効/無効を制御できるようになりました。
  3. printer.goの変更:

    • Printer構造体にhtmlフィールドが追加され、HTML出力モードであるかどうかの状態を保持するようになりました。
    • Printer.Init関数が変更され、htmlフラグを引数として受け取るようになりました。
    • htmlEscape関数がPrinterのメソッドとなり、P.htmlフィールドを参照してHTMLエスケープを行うかどうかを決定します。これにより、HTMLエスケープ処理がPrinterの状態に依存するようになります。
    • TaggedStringHtmlPrologueHtmlEpilogueHtmlIdentifierなどの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.Writerhtmlフラグを引数として受け取るようになりました。これにより、Printerが標準出力だけでなく、任意のio.Writer(例: HTTPレスポンスライター)にHTMLまたはプレーンテキストを出力できるようになります。
  4. compilation.goの変更:

    • ComputeDeps関数内で、ソースファイル名から.go拡張子をトリムするためにUtils.TrimExtが使用されるようになりました。これは、ファイルパス処理の汎用性を高めるための小さな改善です。
  5. 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 = "&amp;";
 	\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.Writerhtmlフラグを引数として受け取るようになりました。これにより、Printerは標準出力だけでなく、HTTPレスポンスライターなど、任意の出力先にHTMLまたはプレーンテキストを書き出すことができるようになり、gdsとの統合を可能にしています。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語の初期の設計に関する議論やメーリングリストのアーカイブ (当時の情報源): Go言語の初期の設計に関する情報は、Goの公式メーリングリストやデザインドキュメントに散見されますが、特定のURLを特定することは困難です。しかし、このコミットがGoのドキュメンテーションツールの基礎を築いたことは、その後のgo docコマンドやpkg.go.devといった公式ドキュメンテーションサイトの発展に繋がっています。