Goの理解:バイトおよび文字列パッケージ

GoウォークスルーシリーズのBen Johnsonの記事の1つを翻訳して、実世界のタスクのコンテキストでのGo標準ライブラリのより詳細な研究を行います。







以前の投稿で、バイトストリームの操作方法を見つけましたが、メモリ内の特定のバイトセットを操作する必要がある場合があります。 バイトスライスは多くのタスクに非常に適していますが、 bytesパッケージを使用したほうがよい場合が多くあります。 また、今日の文字列パッケージも見ていきます。APIはバイトとほぼ同じで、 文字列でのみ機能するためです。







この投稿は、標準ライブラリのより詳細な分析に関する一連の記事の1つです。 標準ドキュメントは多くの有用な情報を提供するという事実にもかかわらず、実際のタスクのコンテキストでは、何をいつ使用するかを把握するのが難しい場合があります。 この一連の記事は、実際のアプリケーションのコンテキストでの標準ライブラリパッケージの使用を示すことを目的としています。 質問やコメントがあれば、いつでもTwitter-@benbjohnsonで私を書くことができます。







文字列とバイトに関する簡単な余談



Rob Pikeは、 文字列、バイト、ルーン、および文字に関する優れた詳細な投稿を書きましたが、この投稿では、開発者の観点からより単純な定義を提供したいと思います。







スライスバイトは、バイトの可変シーケンシャルセットです。 少し冗長なので、これが何を意味するのかを理解してみましょう。







バイトスライスがあります。







buf := []byte{1,2,3,4}
      
      





可変なので、その中の要素を変更できます:







 buf[3] = 5 // []byte{1,2,3,5}
      
      





サイズを変更することもできます:







 buf = buf[:2] // []byte{1,2} buf = append(buf, 100) // []byte{1,2,100}
      
      





また、メモリ内のバイトが次々と移動するため、シーケンシャルです。







 1|2|3|4
      
      





一方 、文字列は固定サイズのバイトの不変の連続セットです。 つまり、行を変更することはできません-新しい行のみを作成してください。 これは、プログラムのパフォーマンスのコンテキストで理解することが重要です。 非常に高いパフォーマンスが必要なプログラムでは、多数の行を絶えず作成すると、ガベージコレクターに顕著な負荷がかかります。







開発者の観点から、文字列はUTF-8でデータを操作するときに最適に使用されます。たとえば、バイトスライスとは異なり、マップのキーとして使用できます。また、ほとんどのAPIは文字列データを表すために文字列を使用します。 一方、バイトスライスは、たとえばデータストリームを処理するときに生のバイトを使用する必要がある場合に適しています。 また、新しいメモリ割り当てを避け、メモリを再利用する場合にも便利です。







ストリームの文字列とスライスの調整



バイトおよび文字列パッケージの最も重要な機能の1つは、メモリ内のバイトおよび文字列を操作するためのio.Readerおよびio.Writerインターフェイスを実装することです。







インメモリーリーダー



Go標準ライブラリで使用されていない2つの関数は、 bytes.NewReaderstrings.NewReaderです。







 func NewReader(b []byte) *Reader func NewReader(s string) *Reader
      
      





これらの関数は、メモリ内のバイトスライスまたは文字列のラッパーとして機能するio.Readerインターフェース実装を返します。 ただし、これらはリーダーだけではありません-io.ReaderAtio.WriterToio.ByteReaderio.ByteScannerio.RuneReaderio.RuneScannerio.Seekerなど、他の関連インターフェースも実装します。







バイトスライスと行が最初にbytes.Bufferに書き込まれ、次にバッファーがリーダーとして使用されるコードを定期的に確認します。







 var buf bytes.Buffer buf.WriteString("foo") http.Post("http://example.com/", "text/plain", &buf)
      
      





このアプローチでは追加のメモリ割り当てが必要であり、時間がかかる場合があります。 string.Readerを使用する方がはるかに効率的です。







 r := strings.NewReader("foobar") http.Post("http://example.com", "text/plain", r)
      
      





このメソッドは、[io.MultiReader]()を使用して組み合わせることができる多くの行またはバイトスライスがある場合にも機能します。







 r := io.MultiReader( strings.NewReader("HEADER"), bytes.NewReader([]byte{0,1,2,3,4}), myFile, strings.NewReader("FOOTER"), )
      
      





インメモリーライター



bytesパッケージは、 Bufferタイプを使用してメモリ内のバイトスライス用のio.Writerインターフェイスも実装します。 io.Closerio.Seekerを除く、 ioパッケージのほぼすべてのインターフェイスを実装します。 また、バッファの最後に行を書き込むためのヘルパーメソッドWriteString()もあります。







ユニットテストでバッファを積極的に使用して、サービスログの出力をキャプチャします。 log.New()の引数としてバッファを渡し、後で出力を確認できます。







 var buf bytes.Buffer myService.Logger = log.New(&buf, "", log.LstdFlags) myService.Run() if !strings.Contains(buf.String(), "service failed") { t.Fatal("expected log message") }
      
      





しかし、量産コードでは、私はめったにBufferを使用しません。 名前にもかかわらず、バッファリングされた読み取りと書き込みには使用しません。これは、標準ライブラリにこれ専用のbufioパッケージがあるためです。







パッケージ構成



一見すると、バイトと文字列のパッケージは非常に大きいように見えますが、実際には単純なヘルパー関数のコレクションにすぎません。 それらをいくつかのカテゴリにグループ化できます。









これらの関数がどのようにグループ化されているかを理解すると、一見大きなAPIがはるかに快適に見えます。







比較関数



2つのバイトスライスまたは2行がある場合、2つの質問に対する答えを取得する必要がある場合があります。 まず、これら2つのオブジェクトは等しいですか? そして2番目-ソートする前にどのオブジェクトが来ますか?







平等



Equal()関数は最初の質問に答えます:







 func Equal(a, b []byte) bool
      
      





文字列は==演算子を使用して比較できるため、この関数はbytesパッケージでのみ使用できます。







同等性のチェックは単純なタスクのように思えるかもしれませんが、 strings.ToUpper()を使用して大文字と小文字を区別しない同等性をチェックする場合によくある間違いがあります。







 if strings.ToUpper(a) == strings.ToUpper(b) { return true }
      
      





このアプローチは間違っています。新しい行に2つの割り当てを使用します。 もっと正確なアプローチはEqualFold()を使用することです







 func EqualFold(s, t []byte) bool func EqualFold(s, t string) bool
      
      





ここでいう「折り畳み」という言葉は、 Unicodeの大文字と小文字の区別を意味します 。 AZだけでなく、他の言語の大文字と小文字の規則をカバーし、φをtoに変換できます。







比較



バイトまたはストリングの2つのスライスをソートする順序を見つけるために、 Compare()関数があります。







 func Compare(a, b []byte) int func Compare(a, b string) int
      
      





この関数は、aがbより小さい場合は-1、aがbより大きい場合は1、aとbが等しい場合は0を返します。 この関数は、バイトとの対称性のためだけに文字列パッケージに含まれています。 ラスコックスは「 strings.Compareを使用すべきではない 」とさえ呼びかけています。 組み込みの演算子<and>を使用する方が簡単です。







「誰もstrings.Compareを使うべきではない」、ラス・コックス


通常、データを並べ替えるときは、バイトまたは文字列のスライスを比較する必要があります。 sort.Interfaceインターフェースには、Less()メソッドの比較関数が必要です。 Compare()の戻り値の3進形式をLess()のブール値に変換するには、-1との等価性をチェックします。







 type ByteSlices [][]byte func (p ByteSlices) Less(i, j int) bool { return bytes.Compare(p[i], p[j]) == -1 }
      
      





検証関数



バイトおよび文字列パッケージは、文字列またはバイトスライスの値をチェックまたは検索するためのいくつかの方法を提供します。







カウント



入力を検証する場合、それらの特定のバイトの存在(または不在)を確認する必要があります。 これを行うには、 Contains()関数を使用できます。







 func Contains(b, subslice []byte) bool func Contains(s, substr string) bool
      
      





たとえば、特定の悪い単語を確認できます。







 if strings.Contains(input, "darn") { return errors.New("inappropriate input") }
      
      





目的の部分文字列の正確な出現回数を見つける必要がある場合は、Count()を使用できます。







 func Count(s, sep []byte) int func Count(s, sep string) int
      
      





Count()のもう1つの用途は、文字列内のルーンの数をカウントすることです。 sep引数として空のスライスまたは空の文字列を渡すと、Count()はルーン数+ 1を返します。これは、バイト数を返すlen()の出力とは異なります。 マルチバイトUnicode文字を使用している場合、この違いは重要です。







 strings.Count("I ", "") // 6 len("I ") // 9
      
      





実際には5つのルーン文字があるため、最初の行は奇妙に見えるかもしれませんが、Count()がルーン文字に1を加えた数を返すことを忘れないでください。







索引付け



エントリの確認は重要なタスクですが、サブストリングまたは目的のスライスの正確な位置を見つける必要がある場合があります。 これは、インデックス関数を使用して実行できます。







 Index(s, sep []byte) int IndexAny(s []byte, chars string) int IndexByte(s []byte, c byte) int IndexFunc(s []byte, f func(r rune) bool) int IndexRune(s []byte, r rune) int
      
      





さまざまなケースに対応する機能がいくつかあります。 インデックス()はマルチバイトスライスを探しています。 IndexByte()は、スライス内の単一バイトを見つけます。 IndexRune()は、UTF-8文字列でUnicodeコードポイントを探します。 IndexAny()はIndexRune()と同様に機能しますが、一度に複数のコードポイントを検索します。 結論として、 IndexRune()を使用すると、独自の関数を使用してインデックスを検索できます。







最後から最初の位置を見つけるための同様の関数セットもあります。







 LastIndex(s, sep []byte) int LastIndexAny(s []byte, chars string) int LastIndexByte(s []byte, c byte) int LastIndexFunc(s []byte, f func(r rune) bool) int
      
      





私は通常、インデックス作成関数を少し使用します。これは、パーサーなどのより複雑なものを作成する必要がある場合が多いためです。







プレフィックス、サフィックス、および削除



プログラミングプレフィックスは非常に一般的です。 たとえば、HTTPアドレスのパスは、多くの場合、プレフィックスを使用して機能ごとにグループ化されます。 または、別の例-行の先頭にある「@」などの特殊文字は、ユーザーを指すために使用されます。







HasPrefix()およびHasSuffix()関数を使用すると、このような場合を確認できます。







 func HasPrefix(s, prefix []byte) bool func HasPrefix(s, prefix string) bool func HasSuffix(s, suffix []byte) bool func HasSuffix(s, suffix string) bool
      
      





これらの関数は単純すぎると思われるかもしれませんが、開発者が文字列のゼロサイズをチェックするのを忘れると、次のエラーが定期的に表示されます。







 if str[0] == '@' { return true }
      
      





このコードは単純に見えますが、strが空の文字列であることが判明すると、パニックになります。 HasPrefix()関数には次のチェックが含まれます。







 if strings.HasPrefix(str, "@") { return true }
      
      





削除する



バイトと文字列の「トリミング」という用語は、スライスまたは行の先頭および/または末尾のバイトまたはルーンを削除することを意味します。 このために一般化された関数自体はTrim()です。







 func Trim(s []byte, cutset string) []byte func Trim(s string, cutset string) string
      
      





ラインの最初と最後から、両側のカットセットからすべてのルーン文字を削除します。 TrimLeft()およびTrimRight()をそれぞれ使用して、先頭からのみ、または末尾からのみ削除することもできます。







しかし、より多くの場合、より具体的な削除オプションが使用されます-スペースを削除するには、 TrimSpace()関数があります:







 func TrimSpace(s []byte) []byte func TrimSpace(s string) string
      
      





「\ n \ r」に等しいカットセットで削除するだけで十分であると思われるかもしれませんが、TrimSpace()はUnicodeで定義されたスペース文字も削除できます。 これには、スペース、ラインフィード、またはタブだけでなく、 「細いスペース」「ヘアスペース」などの非標準文字も含まれます。







TrimSpace()は実際には、削除に使用される文字を定義するTrimFunc()の単なるラッパーです。







 func TrimSpace(s string) string { return TrimFunc(s, unicode.IsSpace) }
      
      





したがって、行の末尾のスペースのみを削除する独自の関数を非常に簡単に作成できます。







 TrimRightFunc(s, unicode.IsSpace)
      
      





結論として、文字ではなく、左側または右側の特定の部分文字列を削除したい場合、 TrimPrefix()およびTrimSuffix()関数があります:







 func TrimPrefix(s, prefix []byte) []byte func TrimPrefix(s, prefix string) string func TrimSuffix(s, suffix []byte) []byte func TrimSuffix(s, suffix string) string
      
      





HasPrefix()およびHasSuffix()関数と連携して 、それぞれプレフィックスまたはサフィックスを確認します。 たとえば、ホームディレクトリにある構成ファイルのパスをbashのように補完するために使用します。







 // Look up user's home directory. u, err := user.Current() if err != nil { return err } else if u.HomeDir == "" { return errors.New("home directory does not exist") } // Replace tilde prefix with home directory. if strings.HasPrefix(path, "~/") { path = filepath.Join(u.HomeDir, strings.TrimPrefix(path, "~/")) }
      
      





置換機能



簡単な交換



場合によっては、部分文字列またはスライスの一部を置き換える必要があります。 最も単純な場合、必要なのはReplace()関数だけです。







 func Replace(s, old, new []byte, n int) []byte func Replace(s, old, new string, n int) string
      
      





それは、文字列内の古いものを新しいものに置き換えます。 nが-1の場合、すべての出現が置き換えられます。 この関数は、単純な単語をパターンに置き換える必要がある場合に非常に適しています。 たとえば、ユーザーに$ NOWパターンの使用を許可し、それを現在の時刻に置き換えることができます。







 now := time.Now().Format(time.Kitchen) println(strings.Replace(data, "$NOW", now, -1)
      
      





複数の異なるオカレンスを一度に置換する必要がある場合は、 strings.Replacerを使用します。 入力として古い/新しい値を取ります:







 r := strings.NewReplacer("$NOW", now, "$USER", "mary") println(r.Replace("Hello $USER, it is $NOW")) // Output: Hello mary, it is 3:04PM
      
      





ケース交換



レジスタを操作するのは簡単だと思うかもしれませんが(下と上、ビジネスだけ)、GoはUnicodeで動作し、Unicodeは決して単純ではありません。 レジスタには、大文字、小文字、大文字の3つのタイプがあります。







上部と下部はほとんどの言語で非常にシンプルで、 ToUpper()およびToLower()関数を使用するだけです。







 func ToUpper(s []byte) []byte func ToUpper(s string) string func ToLower(s []byte) []byte func ToLower(s string) string
      
      





ただし、一部の言語では、レジスタの規則が一般に受け入れられている規則と異なります。 たとえば、トルコ語では、大文字のiİのようになります。 そのような特別な場合のために、これらの関数の特別なバージョンがあります:







 strings.ToUpperSpecial(unicode.TurkishCase, "i")
      
      





さらに、資本登録簿とToTitle()関数がまだあります。







 func ToTitle(s []byte) []byte func ToTitle(s string) string
      
      





ToTitle()がすべての文字を大文字に変換するのを見ると、おそらく非常に驚くでしょう:







 println(strings.ToTitle("the count of monte cristo")) // Output: THE COUNT OF MONTE CRISTO
      
      





これは、ユニコードでは大文字が特殊なケースであり、大文字の単語の最初の文字ではないためです。 ほとんどの場合、大文字と大文字は同じですが、そうではないコードポイントがいくつかあります。 たとえば、 大文字のコードポイントlj (はい、これは1つのコードポイントです)はlikeのように見え、大文字の場合は-Ljです。







この場合に必要な関数は、おそらくTitle()です。







 func Title(s []byte) []byte func Title(s string) string
      
      





彼女の結論はもっと真実に似ているだろう:







 println(strings.Title("the count of monte cristo")) // Output: The Count Of Monte Cristo
      
      





ルーンのマッピング



バイトスライスと文字列のデータを置き換える別の方法があります-Map ()関数:







 func Map(mapping func(r rune) rune, s []byte) []byte func Map(mapping func(r rune) rune, s string) string
      
      





この関数を使用すると、各ルーンをチェックおよび置換する関数を指定できます。 正直に言うと、この投稿の執筆を開始するまでこの機能については知りませんでしたので、ここで個人的な使用履歴を説明することはできません。







結合および分離関数



多くの場合、文字列を分割する必要がある区切り文字を含む文字列を操作する必要があります。 たとえば、UNIXパスはコロンで区切られており、CSV形式は基本的にカンマで区切られたデータです。







改行



スライスまたはサブストリングの単純な分割のために、Split()-関数があります:







 func Split(s, sep []byte) [][]byte func SplitAfter(s, sep []byte) [][]byte func SplitAfterN(s, sep []byte, n int) [][]byte func SplitN(s, sep []byte, n int) [][]byte func Split(s, sep string) []string func SplitAfter(s, sep string) []string func SplitAfterN(s, sep string, n int) []string func SplitN(s, sep string, n int) []string
      
      





これらの関数は、区切り文字に従って文字列またはスライスバイトを分割し、複数のスライスまたはサブストリングとして返します。 After()-関数はサブストリングにセパレーター自体を含め、N()-関数は返されるパーティションの数を制限します:







 strings.Split("a:b:c", ":") // ["a", "b", "c"] strings.SplitAfter("a:b:c", ":") // ["a:", "b:", "c"] strings.SplitN("a:b:c", ":", 2) // ["a", "b:c"]
      
      





行の分割は非常に一般的な操作ですが、これは通常、CSVまたはUNIXパスの形式のファイルを操作するコンテキストで発生します。 そのような場合、 エンコーディング/ csvおよびパスパッケージをそれぞれ使用します。







分類



場合によっては、一連のルーンではなくルーンのセットとしてセパレーターを指定する必要があります。 ここでの最良の例は、単語を異なる長さのスペースに分割することです。 区切り文字としてスペースを使用してSplit()を呼び出すだけで、入力の行に複数のスペースがある場合、出力に空のサブストリングが表示されます。 代わりに、 Fields()関数を使用します。







 func Fields(s []byte) [][]byte
      
      





連続するスペースを1つの区切り文字として扱います。







 strings.Fields("hello world") // ["hello", "world"] strings.Split("hello world", " ") // ["hello", "", "", "world"]
      
      





Fields()関数は、別の関数-FieldsFunc()の単純なラッパーです。これにより、任意の関数を指定して、ルーン文字の区切りを確認できます。







 func FieldsFunc(s []byte, f func(rune) bool) [][]byte
      
      





行連結



データを操作するときによく使用される別の操作は、スライスとラインの結合です。 これにはJoin()関数があります:







 func Join(s [][]byte, sep []byte) []byte func Join(a []string, sep string) string
      
      





私が遭遇したエラーの1つは、開発者が文字列の連結を手動で実装し、次のように記述しようとしていることです。







 var output string for i, s := range a { output += s if i < len(a) - 1 { output += "," } } return output
      
      





このコードの問題は、多くのメモリ割り当てがあることです。 行は不変なので、反復ごとに新しい行が作成されます。 strings.Join()関数は、バイトのスライスをバッファーとして使用し、最後にそれをストリングに変換します。 これにより、メモリ割り当ての数が最小限に抑えられます。







その他の機能



どのカテゴリにも明確に関連付けることができなかった2つの関数があるため、以下に示します。 まず、 Repeat()関数を使用すると、繰り返し要素の文字列を作成できます。 正直なところ、私が使ったのは、ターミナルの出力を分離する行を作成することだけです。







 println(strings.Repeat("-", 80))
      
      





別の関数Runes()は、 UTF-8として解釈される文字列またはバイトスライスのルーンスライスを返します。 行のforループは、不要な割り当てなしでまったく同じことを行うため、この関数を使用したことはありません。







おわりに



バイトスライスと文字列は、Goの基本的なプリミティブです。 これらは、メモリ内のバイトまたはルーンの表現です。 バイトおよび文字列パッケージは、io.Readerおよびio.Writerインターフェース用のアダプターと同様に、多数の補助機能を提供します。







これらのパッケージのAPIサイズが大きいため、これらの便利な機能の多くを見落とすのは非常に簡単ですが、この投稿がこれらのパッケージを理解し、それらが提供する機能について学ぶのに役立つことを願っています。








All Articles