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

[インデックス 15485] ファイルの概要

このコミットは、Go言語の実験的なcookiejarパッケージにおいて、文字列操作の効率と可読性を向上させるための変更です。具体的には、"." + strのような文字列結合とstrings.HasSuffixの組み合わせを、専用のヘルパー関数hasDotSuffixに置き換えることで、不要な文字列アロケーションを削減し、コードをより明確にしています。この変更は機能的な振る舞いを変更するものではなく、主に内部的な改善を目的としています。

コミット

exp/cookiejar: eliminate some "."+str garbage.

It's not a big deal, but it's easy to fix.

R=dsymonds CC=dr.volker.dobler, golang-dev https://golang.org/cl/7425043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3b69efb010259b681cfd00b5dec9fe61d5be55a3

元コミット内容

commit 3b69efb010259b681cfd00b5dec9fe61d5be55a3
Author: Nigel Tao <nigeltao@golang.org>
Date:   Thu Feb 28 13:46:26 2013 +1100

    exp/cookiejar: eliminate some "."+str garbage.

    It's not a big deal, but it's easy to fix.

    R=dsymonds
    CC=dr.volker.dobler, golang-dev
    https://golang.org/cl/7425043

変更の背景

この変更の背景には、Go言語のexp/cookiejarパッケージ内で、特定のドメインマッチングやパブリックサフィックスのチェックにおいて、"." + suffixという形式で文字列を結合し、その結果に対してstrings.HasSuffix関数を使用している箇所が複数存在したことがあります。

コミットメッセージにある「eliminate some "."+str garbage. It's not a big deal, but it's easy to fix.」という記述が示すように、これは大きなパフォーマンス問題を引き起こすものではありませんでしたが、"." + suffixという文字列結合は、その都度新しい文字列を生成し、メモリをアロケートするオーバーヘッドを伴います。特に、これらの操作が頻繁に実行される場合、小さなアロケーションが積み重なり、ガベージコレクションの負荷を増やす可能性があります。

このコミットは、このような冗長な文字列結合とサフィックスチェックのパターンを、より効率的で直接的なhasDotSuffixというヘルパー関数に置き換えることで、コードのクリーンアップとわずかなパフォーマンス改善を図ることを目的としています。これにより、不必要な文字列アロケーションを避け、コードの意図をより明確にすることができます。

前提知識の解説

Go言語のexp/cookiejarパッケージ

exp/cookiejarは、Go言語の標準ライブラリの一部として提供されているnet/http/cookiejarパッケージの実験的な前身、または関連するパッケージです。HTTPクライアントがWebサーバーとの間でクッキーを管理するための機能を提供します。Webブラウザがクッキーを保存し、後続のリクエストで送信するのと同様に、GoのHTTPクライアントもこのcookiejarを使用してクッキーの状態を維持します。

クッキーの管理には、ドメインマッチング(どのドメインのクッキーがどのリクエストに適用されるか)やパスマッチング(どのパスのクッキーが適用されるか)といった複雑なロジックが含まれます。これらのロジックは、RFC 6265などの標準仕様に基づいて実装されます。

RFC 6265: HTTP State Management Mechanism

RFC 6265は、HTTPクッキーの動作を定義する標準仕様です。このRFCには、クッキーの生成、保存、送信に関する詳細なルールが記述されており、特に「ドメインマッチング」と「パスマッチング」のアルゴリズムが重要です。

  • ドメインマッチング (Domain Matching): クッキーがどのドメインに対して有効であるかを判断するルールです。例えば、example.comのクッキーはwww.example.comにも適用される場合がありますが、other.comには適用されません。RFC 6265のセクション5.1.3で定義されています。このコミットで変更されたdomainMatch関数は、このルールの一部を実装しています。
  • パブリックサフィックス (Public Suffix): com, org, co.ukなどのドメイン名の末尾部分で、個人や組織が直接登録できない部分を指します。パブリックサフィックスリスト(Public Suffix List)は、クッキーのセキュリティを強化するために使用されます。例えば、example.comのクッキーがcom全体に設定されることを防ぎます。RFC 6265のセクション5.3のポイント5で言及されており、psList(Public Suffix List)が関連します。

Go言語の文字列操作とstrings.HasSuffix

Go言語では、文字列は不変(immutable)です。文字列を結合したり、部分文字列を抽出したりする操作は、新しい文字列を生成し、そのためのメモリをアロケートします。

  • strings.HasSuffix(s, suffix string) bool: この関数は、文字列sが指定されたsuffixで終わるかどうかをチェックします。例えば、strings.HasSuffix("filename.txt", ".txt")trueを返します。
  • 文字列結合 (+演算子): s1 + s2のように+演算子を使って文字列を結合すると、新しい文字列が作成されます。例えば、"." + "example.com"".example.com"という新しい文字列を生成します。

このコミットの変更は、"." + suffixという結合によって一時的な文字列が生成されることを避け、直接バイト列を比較することで効率を向上させています。

技術的詳細

このコミットの主要な技術的変更は、strings.HasSuffix(host, "."+e.Domain)というパターンを、新しく導入されたヘルパー関数hasDotSuffix(s, suffix string)に置き換えたことです。

元のコードでは、host文字列が"."e.Domainを結合した文字列で終わるかどうかをチェックしていました。例えば、host"www.example.com"e.Domain"example.com"の場合、"." + e.Domain".example.com"という新しい文字列を生成し、その後strings.HasSuffix("www.example.com", ".example.com")が呼び出されます。この".example.com"という一時的な文字列の生成が、メモリのアロケーションとガベージコレクションの対象となります。

新しいhasDotSuffix関数は、この一時的な文字列生成を回避します。その実装は以下の通りです。

func hasDotSuffix(s, suffix string) bool {
    return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
}

この関数は、以下の3つの条件をチェックします。

  1. len(s) > len(suffix): sの長さがsuffixの長さよりも長いことを確認します。これは、ssuffixの前に少なくとも1文字(この場合は.)を持つことを保証するためです。
  2. s[len(s)-len(suffix)-1] == '.': sの末尾からsuffixの長さ分を遡った直前の文字が.であることをチェックします。これにより、s... .suffixという形式であることを確認します。
  3. s[len(s)-len(suffix):] == suffix: sの末尾len(suffix)文字がsuffixと完全に一致することをチェックします。これは、strings.HasSuffixが内部で行うのと同様の比較です。

これらの条件を組み合わせることで、s"."に続いてsuffixで終わることを、新しい文字列をアロケートすることなく効率的に判断できます。これは、文字列のインデックスアクセスとスライス操作を直接利用することで実現されています。

この変更は、特にdomainMatchdomainAndTypeのような頻繁に呼び出される可能性のある関数において、わずかながらもパフォーマンスの改善とメモリ使用量の削減に貢献します。また、コードの意図がhasDotSuffixという関数名によってより明確になります。

コアとなるコードの変更箇所

src/pkg/exp/cookiejar/jar.go

--- a/src/pkg/exp/cookiejar/jar.go
+++ b/src/pkg/exp/cookiejar/jar.go
@@ -121,7 +121,7 @@ func (e *entry) domainMatch(host string) bool {
 	if e.Domain == host {
 		return true
 	}
-	return !e.HostOnly && strings.HasSuffix(host, "."+e.Domain)
+	return !e.HostOnly && hasDotSuffix(host, e.Domain)
 }
 
 // pathMatch implements "path-match" according to RFC 6265 section 5.1.4.
@@ -139,6 +139,11 @@ func (e *entry) pathMatch(requestPath string) bool {
 	return false
 }
 
+// hasDotSuffix returns whether s ends in "."+suffix.
+func hasDotSuffix(s, suffix string) bool {
+	return len(s) > len(suffix) && s[len(s)-len(suffix)-1] == '.' && s[len(s)-len(suffix):] == suffix
+}
+
 // byPathLength is a []entry sort.Interface that sorts according to RFC 6265
 // section 5.4 point 2: by longest path and then by earliest creation time.
 type byPathLength []entry
@@ -469,7 +474,7 @@ func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
 
 	// See RFC 6265 section 5.3 #5.
 	if j.psList != nil {
-		if ps := j.psList.PublicSuffix(domain); ps != "" && !strings.HasSuffix(domain, "."+ps) {
+		if ps := j.psList.PublicSuffix(domain); ps != "" && !hasDotSuffix(domain, ps) {
 			if host == domain {
 				// This is the one exception in which a cookie
 				// with a domain attribute is a host cookie.
@@ -481,7 +486,7 @@ func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
 
 	// The domain must domain-match host: www.mycompany.com cannot
 	// set cookies for .ourcompetitors.com.
-	if host != domain && !strings.HasSuffix(host, "."+domain) {
+	if host != domain && !hasDotSuffix(host, domain) {
 		return "", false, errIllegalDomain
 	}
 

src/pkg/exp/cookiejar/jar_test.go

--- a/src/pkg/exp/cookiejar/jar_test.go
+++ b/src/pkg/exp/cookiejar/jar_test.go
@@ -40,6 +40,80 @@ func newTestJar() *Jar {
 	return jar
 }
 
+var hasDotSuffixTests = [...]struct {
+	s, suffix string
+}{
+	{"", ""},
+	{"", "."},
+	{"", "x"},
+	{".", ""},
+	{".", "."},
+	{".", ".."},
+	{".", "x"},
+	{".", "x."},
+	{".", ".x"},
+	{".", ".x."},
+	{"x", ""},
+	{"x", "."},
+	{"x", ".."},
+	{"x", "x"},
+	{"x", "x."},
+	{"x", ".x"},
+	{"x", ".x."},
+	{".x", ""},
+	{".x", "."},
+	{".x", ".."},
+	{".x", "x"},
+	{".x", "x."},
+	{".x", ".x"},
+	{".x", ".x."},
+	{"x.", ""},
+	{"x.", "."},
+	{"x.", ".."},
+	{"x.", "x"},
+	{"x.", "x."},
+	{"x.", ".x"},
+	{"x.", ".x."},
+	{"com", ""},
+	{"com", "m"},
+	{"com", "om"},
+	{"com", "com"},
+	{"com", ".com"},
+	{"com", "x.com"},
+	{"com", "xcom"},
+	{"com", "xorg"},
+	{"com", "org"},
+	{"com", "rg"},
+	{"foo.com", ""},
+	{"foo.com", "m"},
+	{"foo.com", "om"},
+	{"foo.com", "com"},
+	{"foo.com", ".com"},
+	{"foo.com", "o.com"},
+	{"foo.com", "oo.com"},
+	{"foo.com", "foo.com"},
+	{"foo.com", ".foo.com"},
+	{"foo.com", "x.foo.com"},
+	{"foo.com", "xfoo.com"},
+	{"foo.com", "xfoo.org"},
+	{"foo.com", "foo.org"},
+	{"foo.com", "oo.org"},
+	{"foo.com", "o.org"},
+	{"foo.com", ".org"},
+	{"foo.com", "org"},
+	{"foo.com", "rg"},
+}
+
+func TestHasDotSuffix(t *testing.T) {
+	for _, tc := range hasDotSuffixTests {
+		got := hasDotSuffix(tc.s, tc.suffix)
+		want := strings.HasSuffix(tc.s, "."+tc.suffix)
+		if got != want {
+			t.Errorf("s=%q, suffix=%q: got %v, want %v", tc.s, tc.suffix, got, want)
+		}
+	}
+}
+
 var canonicalHostTests = map[string]string{
 	"www.example.com":         "www.example.com",
 	"WWW.EXAMPLE.COM":         "www.example.com",

コアとなるコードの解説

src/pkg/exp/cookiejar/jar.go の変更

  1. hasDotSuffix関数の追加: jar.goファイルに、新しいヘルパー関数hasDotSuffix(s, suffix string) boolが追加されました。この関数は、文字列s"."に続いてsuffixで終わるかどうかを効率的に判定します。前述の「技術的詳細」セクションで説明したように、文字列の長さチェックとインデックスアクセスを組み合わせることで、一時的な文字列アロケーションを避けています。

  2. 既存コードのhasDotSuffixへの置き換え:

    • domainMatch関数内のstrings.HasSuffix(host, "."+e.Domain)hasDotSuffix(host, e.Domain)に置き換えられました。domainMatchは、クッキーのドメインがリクエストのホストと一致するか、またはホストがクッキーのドメインのサブドメインであるかを判断するために使用されます。
    • domainAndType関数内の2箇所で、同様にstrings.HasSuffixと文字列結合のパターンがhasDotSuffixに置き換えられました。この関数は、クッキーのドメイン属性が有効であるか、およびホストとドメインの関係を検証するために使用されます。特に、パブリックサフィックスのチェック(RFC 6265 section 5.3 #5)と、ドメインがホストのドメインマッチングルールに従っているかのチェックに関連します。

これらの変更により、cookiejarパッケージの内部で発生していた小さな文字列アロケーションが削減され、コードの意図がより明確になりました。

src/pkg/exp/cookiejar/jar_test.go の変更

  1. hasDotSuffixTests変数の追加: hasDotSuffix関数の動作を検証するための広範なテストケースが定義された構造体スライスhasDotSuffixTestsが追加されました。このテストケースには、空文字列、単一文字、ドットを含む文字列、一般的なドメイン名など、様々なssuffixの組み合わせが含まれています。これにより、hasDotSuffix関数が期待通りに動作することを確認できます。

  2. TestHasDotSuffix関数の追加: hasDotSuffix関数をテストするためのTestHasDotSuffix関数が追加されました。このテスト関数は、hasDotSuffixTests内の各テストケースをループし、hasDotSuffix(tc.s, tc.suffix)の戻り値と、元のstrings.HasSuffix(tc.s, "."+tc.suffix)の戻り値を比較します。両者が一致しない場合にエラーを報告します。これにより、新しいhasDotSuffix関数が、置き換え前のロジックと完全に同じ結果を返すことが保証されます。

テストの追加は、リファクタリングにおいて非常に重要です。新しい関数が既存の振る舞いを壊していないことを確認するための安全網となります。

関連リンク

参考にした情報源リンク