Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Goでホスト名とポート番号の操作をするときはnetパッケージの関数が使える

標準のnetパッケージには、ホスト名とポート番号を操作する関数がいくつか用意されていますが意外と知られていないようなので、便利だけどあまり知られていない関数を3つ紹介します。

TL;DR

ホスト名とポート番号を:で区切られたアドレスに変換する

Goでコードを書くとき、localhost:8080のようにホスト名とポート番号を:で区切ったひとつの文字列として表現することがあります。この表記はAddressと呼ばれており、例えば以下のような関数で使われています。

package http

func ListenAndServe(addr string, handler Handler) error
package net

func Dial(network, address string) (Conn, error)

type Addr interface {
    String() string  // string form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80")
}

ホスト名とポート番号をアドレスへ変換するとき、fmt.Sprintfを使ったコードをよく見かけますが、ホストとしてIPv6アドレスが与えられた場合に意図しない動作を引き起すことがあります。IPv6アドレスは:で区切って表記するので、単純に結合してしまうとIPv6アドレスとポート番号の区別がつかなくなり不正なアドレスとなります。

var (
    host = "::1"
    port = 80
)
addr := fmt.Sprintf("%s:%d", host, port)
c, err := net.Dial("tcp", addr) // err = "dial tcp: address ::1:80: too many colons in address"

正しくは、IPv6アドレスにポート番号を連結する場合は[::1]:80のようにIPv6アドレスの部分を[]で囲む必要があります。この書式はRFC 4038

Therefore, the IP address parsers that take the port number separated with a colon should distinguish IPv6 addresses somehow. One way is to enclose the address in brackets, as is done with Uniform Resource Locators (URLs) [RFC2732]; for example, http://[2001:db8::1]:80.

と定められています。Goのnetパッケージには、この操作を行うnet.JoinHostPort関数が用意されているので、なるべくこちらを使いましょう。ただし、このときportstringなので型の変換が必要です。

addr := net.JoinHostPort(host, strconv.Itoa(port))
c, err := net.Dial("tcp", addr)

ホスト名とポート番号を分割する

上記とは逆に、:で接続された文字列をホスト名とポート番号に分割したい場合は、こちらもnet.SplitHostPortが用意されているのでそのまま使えます。

host, port, err := net.SplitHostPort("[::1]:80")
if err != nil {
    log.Fatalln(err)
}
fmt.Println(host, port) // ::1 80

ポート番号を文字列から数値にする

これはstrconv.Atoistrconv.ParseUintでもそこまで困らないかなと思いますが、net.LookupPortが便利に使えます。strconv.Atoiとの違いは、

  • 負数の場合、または65535より大きい値の場合はエラーになる
  • redisなどサービス名も対応している

といった特徴があります。

_, err := net.LookupPort("tcp", "-1") // err = "address -1: invalid port"
_, err := net.LookupPort("tcp", "65536") // err = "address 65536: invalid port"

port, err := net.LookupPort("tcp", "redis")
if err != nil {
    log.Fatalln(err)
}
fmt.Println(port) // 6379