2022/09/07


Go ではバイト列と文字列は異なる内部データとして扱っています。[]byte から string へ変換したり、またその逆を行う際にはキャストが必要になります。ですので string はイミュータブルになります。

しかしイミュータブルなのは理解しつつもバイト列を文字列にする為に無駄なアロケートをしたくない場合もあります。これまで Go ではドキュメントに明文化していなかった為に色々な作法が生まれてしまっていました。その代表的な物が以下です。

s := *(*string)(unsafe.Pointer(&b))

本来は、Go のバイト列の内部は SliceHeader という構造体により管理されています。

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

また string は以下の StringHeader で管理されています。

type StringHeader struct {
    Data uintptr
    Len  int
}

上記の unsafe.Pointer を使ったコードはこの struct の先頭2フィールドを無理やり参照する事で実現しています。実際に Go のコードでも使われており、これが公式の方法だと思って多くの人が使ってしまっていました。

go/builder.go at 3058d38632aea679c96cd41156b2751c97578a2d · golang/go · GitHub
https://github.com/golang/go/blob/3058d38632aea679c96cd41156b2751c97578a2d/src/strings/builder.go#L30
Sign in to GitHub · GitHub
https://github.com/search?l=Go&q=reflect.StringHeader&type=Code
Sign in to GitHub · GitHub
https://github.com/search?l=Go&q=reflect.SliceHeader&type=Code

しかしながらこの実装は、今後 Go の文字列とバイト列に関する最適化の妨げになり得ます。例えば StringHeader と SliceHeader の交換を最適化する事ができなくなります。

そこで今回、unsafe.StringData、unsafe.String、unsafe.SliceData が入りました。これによりバイト列から文字列、またはその逆がミュータブルに変換できる様になりました。当然ですがこれらはバイト列の破壊的な変更により意図しない文字列の変更が行われる為、慎重に取り扱わなければなりません。

func StringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

func BytesToString(b []bytestring {
    return unsafe.String(&b[0], len(b))
}

unsafe.StringData は string が持っているバイト列へのポインタが返されます。

fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello1"))

これを実行すると以下の様に表示され、Go のコンパイラが文字列リテラル Hello の2つをまとめて保持しているのが分かります。

0xc67760
0xc67760
0xc6790b

また文字列がスタックに取られている時にはメモリ破壊が発生しますのでランタイムでクラッシュします。

s := "hello"
b := StringToBytes(s)
b[0] = 'h'
fmt.Println(s)

以下の様にヒープにメモリが確保されている場合には期待通りに動作します。

s := string([]byte("hello"))
b := StringToBytes(s)
b[0] = 'h'
fmt.Println(s)

動的なアロケーションが発生しないため高速な変換が可能です。おそらく今後、Go の内部実装の多くがこれらで置き換わっていき、幾らか高速化が見込まれます。

Posted at by



2022/08/18


Go言語で画像ファイルか確認してみる - Qiita

Go言語で書かれたサーバーサイドでアップロードされたファイルが画像ファイルか確認する必要があったため今後の備忘録として記載します。

https://qiita.com/tebakane/items/b7a47379659d42364c8d

実は Go にはそれ専用の関数が用意されています。

http package - net/http - Go Packages

Package http provides HTTP client and server implementations. Get, Head, Post, and PostFo...

https://pkg.go.dev/net/http#DetectContentType

バイト列を指定して以下の様に呼び出すだけです。

ct := http.DetectContentType(b)
if strings.HasPrefix(ct, "image/") {
    // 画像ファイル
}

簡単ですね。image.Decode を使い画像を一定のサイズにリサイズしてディスクに保存するといったケースであれば引用元の方法でも良いですが、そうでないならば画像のマジック部分だけで判定できるの DetectContentType を使った方が良いでしょう。

Posted at by



2022/04/01


Go で URL を扱う時は通常、net/url を使います。その際、例えばベースとなる URL にパスを足そうと思うと意外としんどかったりしました。

package main

import (
    "fmt"
    "log"
    "net/url"
    "path"
)

func main() {
    endpoint := "https://example.com"
    u, err := url.Parse(endpoint)
    if err != nil {
        log.Fatal(err)
    }
    u.Path = path.Join(u.Path, "foo""bar""baz")
    endpoint = u.String()
    fmt.Println(endpoint)
}

一部の方は「ハァッ?文字列で足せばいいじゃん」と思われるかもしれませんが、URL のパスを扱うという行為は実はとても難しい事なのです。path.Join を使う事で、不要な / が混じらない効果があります。また以下の様に相対パスを扱う場合

u.Path = path.Join(u.Path, "foo""..""baz")

https:///example.com/foo/baz に修正してくれるといった効果もあります。そういう点で僕は Go で URL を扱う場合は面倒でも url.Parse を使う事をオススメしています。しかしたかが元が文字列の URL にパスを足す為だけにこれだけのコードを書くのは、いくら Explicit の精神とは言え面倒に感じるのは共感できます。

net/url: add JoinPath, URL.JoinPath · golang/go@604140d · GitHub

Builds on CL 332209. Fixes #47005 Change-Id: I82708dede05d79a196ca63f5a4e7cb5ac9a041ea GitHub-Last-R...

https://github.com/golang/go/commit/604140d93111f89911e17cb147dcf6a02d2700d0
Go の master ブランチで net/urlJoinPathURL.JoinPath が追加されました。上記のコードであれば以下の様に簡単にパスを結合する事ができる様になりました。 package main

import (
    "fmt"
    "log"
    "net/url"
)

func main() {
    endpoint := "https://example.com"
    var err error
    endpoint, err = url.JoinPath(endpoint, "foo""bar")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(endpoint)
}

URL のパスを組み立てる為に URL を作らなくても良いので、ずいぶん楽になりました。

Posted at by