🔗

[Go]net/urlパッケージを見てみる

2025/01/01に公開

記事の概要

業務にてGo言語の標準パッケージnet/urlパッケージを使用することがあったので、改めてnet/urlパッケージのドキュメントやコードを読んでまとめてみました。よく使用しそうな関数やメソッドをまとめておりますので、参考程度に見ていただければと思います。

対象読者

  • Go言語を多少なりとも書ける方
  • Go言語でurl操作をしたい方
  • そういえばnet/urlパッケージ気になってたわ、という方

参考

net/urlパッケージを見てみる

そもそもnet/urlパッケージとは何をするものなのかというと、urlを解析したり、エスケープ処理をしたり、urlの内容を変更したり、url関連の操作をするパッケージになります。

Package url parses URLs and implements query escaping.

早速見ていきます。

func JoinPath

JoinPathは指定されたパス要素をbaseの既存のパスに結合し、./../などの不要な要素を取り除いたurl文字列を返す。

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

func PathEscape

PathEscapeは文字列をurlパスセグメント内に安全に配置できるようにエスケープをして、必要に応じて特殊文字をパーセントエンコーディングする

func PathEscape(s string) string
example
func main() {
  result := url.PathEscape("my/cool+blog&about, stuff")
  fmt.Println(result) // my%2Fcool+blog&about%2C%20stuff
}

func PathUnescape

PathUnescapePathEscapeの逆変換で、パーセントエンコーディングでエスケープされた文字列を、エスケープ前の状態に戻す

func PathUnescape(s string) (string, error)
example
func main() {
  result, err := url.PathUnescape("my%2Fcool+blog&about%2Cstuff")
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(result) // my/cool+blog&about, stuff
}

func QueryEscape

QueryEscapeはurlクエリ内に安全に配置できるよう、文字列をエスケープする

func QueryEscape(s string) string
example
func main() {
  result := url.QueryEscape("my/cool+blog&about, stuff")
  fmt.Println(result) // my%2Fcool%2Bblog%26about%2C+stuff
}

func QueryUnescape

QueryUnescapeQueryEscapeの逆変換で、パーセントエンコーディングでエスケープされた文字列を、エスケープ前の状態に戻す

func QueryUnescape(s string) (string, error)
example
func main() {
  result, err := url.QueryUnescape("my%2Fcool%2Bblog%26about%2C+stuff")
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println(result) // my/cool+blog&about, stuff
}

type URL

URLは解析(parse)されたurlを表す構造体。

type URL struct {
  Scheme      string
  Opaque      string    // encoded opaque data
  User        *Userinfo // username and password information
  Host        string    // host or host:port (see Hostname and Port methods)
  Path        string    // path (relative paths may omit leading slash)
  RawPath     string    // encoded path hint (see EscapedPath method)
  OmitHost    bool      // do not emit empty host (authority)
  ForceQuery  bool      // append a query ('?') even if RawQuery is empty
  RawQuery    string    // encoded query values, without '?'
  Fragment    string    // fragment for references, without '#'
  RawFragment string    // encoded fragment hint (see EscapedFragment method)
}

一般的にURLの構造は以下のようになっているので、それにフィールドが対応している感じですね。

[scheme:][//[userinfo@]host][/]path[?query][#fragment]
example
func main() {
  u, err := url.Parse("http://bing.com/search?q=dotnet")
  if err != nil {
    log.Fatal(err)
  }
  u.Scheme = "https"
  u.Host = "google.com"
  q := u.Query()
  q.Set("q", "golang")
  u.RawQuery = q.Encode()
  fmt.Println(u) // https://google.com/search?q=golang
}

実際に各フィールドがどのように出力されるかが気になる方は以下の記事が参考になります。

func Parse

Parseは生のurlを解析してURL構造体に変換する

func Parse(rawURL string) (*URL, error)

func ParseRequestURI

ParseRequestURIは生のurlを解析して、URL構造体に変換する

func ParseRequestURI(rawURL string) (*URL, error)

func (*URL) String

StringURL構造体を有効なurl文字列に再構築する

func (u *URL) String() string

func (*URL) IsAbs

IsAbsは絶対パス(スキームから始まるパス)かどうかを検証する

func (u *URL) IsAbs() bool

type Values

Valuesは文字列と文字列のリストを持つmapの型で、主にはクエリパラメータやフォームの値に使用される

type Values map[string][]string
example
func main() {
  v := url.Values{}
  v.Set("name", "Ava")
  v.Add("friend", "Jess")
  v.Add("friend", "Sarah")
  v.Add("friend", "Zoe")
  // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
  fmt.Println(v.Get("name")) // Ava
  fmt.Println(v.Get("friend")) // Jess
  fmt.Println(v["friend"]) // [Jess Sarah Zoe]
}

func (Values) Set

Setはキーと値をセットする(既存の値は入れ替わる)

func (v Values) Set(key, value string)
example
func main() {
  v := url.Values{}
  v.Add("cat sounds", "meow")
  v.Add("cat sounds", "mew")
  v.Add("cat sounds", "mau")
  fmt.Println(v["cat sounds"]) // [meow mew mau]

  v.Set("cat sounds", "meow")
  fmt.Println(v["cat sounds"]) // [meow]
}

func (Values) Add

Addはキーに対して値を追加する(Setとは違い、既存の値は入れ替わらない)

func (v Values) Add(key, value string)

func (Values) Del

Delは指定したキーの値を削除する

func (v Values) Del(key string)
example
func main() {
  v := url.Values{}
  v.Add("cat sounds", "meow")
  v.Add("cat sounds", "mew")
  v.Add("cat sounds", "mau")
  fmt.Println(v["cat sounds"]) // [meow mew mau]

  v.Set("cat sounds", "meow")
  fmt.Println(v["cat sounds"]) // []
}

func (Values) Get

Getは指定されたキーの最初の値を取得する
キーに関連付けられた値がない場合、Getは空の文字列を返す

func (v Values) Get(key string) string
example
func main() {
  v := url.Values{}
  v.Add("cat sounds", "meow")
  v.Add("cat sounds", "mew")
  v.Add("cat sounds", "mau")
  fmt.Printf("%q\n", v.Get("cat sounds")) // "meow"
  fmt.Printf("%q\n", v.Get("dog sounds")) // ""
}

func (Values) Has

Hasは指定されたキーが設定されているかをチェックする

func (v Values) Has(key string) bool
example
func main() {
  v := url.Values{}
  v.Add("cat sounds", "meow")
  v.Add("cat sounds", "mew")
  v.Add("cat sounds", "mau")
  fmt.Printf(v.Has("cat sounds")) // true
  fmt.Printf(v.Has("dog sounds")) // false
}

補足

PathEscapeとQueryEscapeの違い

PathEscapeQueryEscapeは同じくエスケープの処理なのですが、一部挙動が異なります。
というのも、「パスでは/をエンコードしたくない」「クエリパラメータではスペースを+に変換したい」など、それぞれのセグメントに応じて使い分けが必要になることもあるので、それに合わせてエンコードをいい感じにやってくれる、ということになります。

なので、実装の際には用途に合わせて使い分けるのが無難そうですね。

ParseとParseRequestURIの違い

両方とも受け取った文字列のurlをURL構造体に変換している処理ですが、少しだけ用途が変わってきます。ドキュメントのコメントを読んでみると、

Parse parses a raw url into a URL structure.
The url may be relative (a path, without a host) or absolute (starting with a scheme). Trying to parse a hostname and path without a scheme is invalid but may not necessarily return an error, due to parsing ambiguities.

ParseRequestURI parses a raw url into a URL structure. It assumes that url was received in an HTTP request, so the url is interpreted only as an absolute URI or an absolute path. The string url is assumed not to have a #fragment suffix. (Web browsers strip #fragment before sending the URL to a web server.)

というふうに、相対パスを許容するかしないかの違いでした。

  • Parse()
    • 相対パスを許容
  • ParseRequestURI()
    • 相対パスを許容せずエラーを返す
    • 絶対パス(スキームから始まるパス)のみ許容する

なので、urlの解析を絶対パスを想定しているか、相対パスを想定しているかで用途を分けるイメージが正しそうです。

コード内でurlを組み立てる時

プログラム内でurlを組み立てる時も少し注意が必要です。
以下のように文字列を組み立てる方法でも可能ではあるのですが、

  • url := fmt.Sprintf("%s/%s", endpoint, path)
  • url := endpoint + "/" + path

これだと万が一/が一つ多い状態で来たり、urlで扱えない文字が来た時の想定ができてません。
なので、url.JoinPath()path.Join()などを使用してちゃんと対策をするほうが無難ではあります。

URL構造体を使用して初期化をすることも筆者としてはオススメです。

url := &url.URL{
  Scheme: "https",
  Host:   "example.com",
  Path:   "hoge/hoge",
}

Discussion