4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

path.Join() と url.JoinPath() の違いを知った話

Last updated at Posted at 2025-10-19

はじめに

OSSのシナリオテストツール runn を使っていて、小さな問題に遭遇しました。

その問題を調べて修正する過程で、path.Join()url.JoinPath()という似た名前だけど用途が全く違うものの存在を知ったので、備忘録として残します。

遭遇した問題: trailing slashが消える

問題の内容

runnでHTTPリクエストのテストを書いているとき、こんな現象に遭遇しました。

# テストシナリオ
runners:
  api:
    endpoint: https://api.example.com

steps:
  - api:
      /users/:  # 末尾のスラッシュを指定
        post:
          body:
            username: taro

期待: POST /users/ にリクエスト
実際: POST /users にリクエスト(末尾のスラッシュが消える

trailing slashの有無を厳密に区別するAPI(スラッシュなしだと404を返すなど)をrunnでテストしようとしたときに、末尾のスラッシュが消えてしまい困っていました。

そこで、原因を調べることにしました。

原因を調べる

ソースコードを読む

実はGo言語自体書いた経験が少なくて、結構手探りでしたが、URLを結合している部分を探しました。

func mergeURL(u *url.URL, p string) (*url.URL, error) {
    // ...省略...
    m.Path = path.Join(m.Path, a.Path)  // ← ここでパスを結合
    // ...省略...
}

引用元: https://github.com/k1LoW/runn/blob/da184c3daacab33b881b806becb86f0e1395ef8c/http.go#L595

path.Join() という関数が使われている。

path.Join() を調べる

公式ドキュメントを読むと、以下のように結果が正規化されると書いてあります。

The result is Cleaned.

引用元: https://pkg.go.dev/path#Join

実際のソースコードを見ると、Join() は内部で Clean() を呼び出しています。

// path/path.go より一部抜粋
func Join(elem ...string) string {
  // ...省略...
	return Clean(strings.Join(elem, string(Separator)))
}

引用元: https://github.com/golang/go/blob/go1.22.0/src/path/path.go#L172

さらに path.Clean() のドキュメントには、以下のようなことが書かれていて、ルート以外は末尾のスラッシュが削除される仕様でした。

The returned path ends in a slash only if it is the root "/".

引用元: https://pkg.go.dev/path#Clean

実際に試してみると:

fmt.Println(path.Join("/api", "/users/"))  // → /api/users (スラッシュ消える)

これが原因だとわかりました。

自分なりの解決策

最初は preserveTrailingSlash オプションを追加することを提案したのですが、メンテナーの方から「デフォルトの動作を変更すべき」とフィードバックをいただいたので、修正を試みることにしました。

参考: Issue #1323 でのやり取り

まず、こう考えました。

「スラッシュが消えるなら、消える前に覚えておいて、後から手動で追加すればいい?」

// 自分で考えた解決策
hasTrailing := strings.HasSuffix(a.Path, "/")
m.Path = path.Join(m.Path, a.Path)
if hasTrailing && !strings.HasSuffix(m.Path, "/") {
    m.Path += "/"  // 手動でスラッシュを追加
}

これでテストも通ったので、PRを作成しました。

メンテナーの方からのフィードバック

すると、メンテナーの方からこんなコメントが:

I recommend using the following function:
https://pkg.go.dev/net/url#JoinPath

「え、そんな関数あるの!?」

url.JoinPath() という関数の存在を知りませんでした...

url.JoinPath() を知る

調べてみた

https://pkg.go.dev/net/url#URL.JoinPath

func (u *URL) JoinPath(elem ...string) *URL

JoinPath returns a new URL with the provided path elements joined...

URL用のパス結合メソッド?

Go 1.19で追加されたメソッドらしいです。

実際に動かしてみた

u, _ := url.Parse("http://example.com/api")

// ルートパス "/" を結合
m1 := u.JoinPath("/")
fmt.Println(m1.Path)  // → /api/ (スラッシュが追加される!)

// 通常のパス
m2 := u.JoinPath("/users")
fmt.Println(m2.Path)  // → /api/users

URL用のメソッドなので、URLとして適切に処理してくれる

手動でスラッシュを追加する必要がなくなりました。

path.Join() と url.JoinPath() の違い

この経験を通じて学んだ違いをまとめます。

1. 使い方の違い

関数 説明 パッケージ
path.Join() スラッシュ区切りのパス結合(関数) path
url.JoinPath() URL のパス結合(URL のメソッド) net/url

2. trailing slashの扱いが違う

path.Join() の場合:

fmt.Println(path.Join("/api", "/users/"))  // → /api/users (常に削除)

path.Join() は結果を Clean() で正規化するため、trailing slash が削除されます。

url.JoinPath() の場合:

u, _ := url.Parse("http://example.com/users")
m := u.JoinPath("/")
fmt.Println(m.Path)  // → /users/ (trailing slashが付く)

URL 専用のメソッドなので、path.Join() とは異なる動作をします。

3. 返り値の型が違う

// path.Join() → string
s := path.Join("/a", "/b")

// url.JoinPath() → *url.URL
u, _ := url.Parse("http://example.com")
newURL := u.JoinPath("/a", "/b")

修正後のコード

メンテナーの方のアドバイスで、こうシンプルになりました。

// Before: 手動でtrailing slashを処理
hasTrailing := strings.HasSuffix(a.Path, "/")
m.Path = path.Join(m.Path, a.Path)
if hasTrailing && !strings.HasSuffix(m.Path, "/") {
    m.Path += "/"
}

// After: url.JoinPath() を使う
m := u.JoinPath(a.Path)

コードがシンプルになり、より適切な関数/メソッドを使えるようになりました。

まとめ

小さな問題でしたが、Goを書いた経験が少なかったので、調べる過程でいろいろ勉強になりました。

特に、似た名前だけど動作が違うものがあることを知りました。

  • path.Join() → trailing slash を削除する
  • url.JoinPath() → URL 専用のメソッド

URL を扱う場合は url.JoinPath() を使うほうが良さそうですね。

参考

Go公式ドキュメント

ソースコード

関連Issue/PR

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?