20190730のGoに関する記事は5件です。

Golangで郵便番号から住所を取得するAPIを作成する

1. Google Maps API の登録と設定をする

https://cloud.google.com/maps-platform/?hl=ja

※ 今回使用するのは Geocoding API

2. Google Maps API を直接叩いてみる

以下のような URL でアクセスして、JSON形式で値が返ってきていれば OK.

https://maps.googleapis.com/maps/api/geocode/json?address=105-0003&language=ja&sensor=false&key=APIキー

参考

3. Go で上述のAPIを呼ぶ

基本的には以下のサンプルをもとに進めれば良い。

https://github.com/googlemaps/google-maps-services-go/blob/master/examples/geocoding/cmdline/main.go

ただ、はじめはなるべくミニマムで動かしたいので最低限の記述に変更する。

main.go
package main

import (
    "context"
    "flag"
    "fmt"
    "log"

    "googlemaps.github.io/maps"
)

type Address struct {
    Prefecture string `json:"prefecture"`
    Locality   string `json:"locality"`
}

var (
    apiKey   = flag.String("key", "APIキー", "API Key for using Google Maps API.")
    language = flag.String("language", "ja", "The language in which to return results.")
    region   = flag.String("region", "JP", "The region code, specified as a ccTLD two-character value.")
)

func main() {
    flag.Parse()

    var client *maps.Client
    var err error

    client, err = maps.NewClient(maps.WithAPIKey(*apiKey))
    outputFatalLog(err)

    req := &maps.GeocodingRequest{
        Address:  "105-0003",
        Language: *language,
        Region:   *region,
    }
    resp, err := client.Geocode(context.Background(), req)
    outputFatalLog(err)

    address := Address{
        Prefecture: resp[0].AddressComponents[2].LongName,
        Locality:   resp[0].AddressComponents[1].LongName,
    }

    fmt.Println(address)
}

func outputFatalLog(err error) {
    if err != nil {
        log.Fatalf("fatal error: %s", err)
    }
}

go run main.go を実行して {東京都 港区} と出力されれば OK.

4. API っぽくする

3 の状態では、固定された住所(東京都港区)しか返却できないので、受け取った郵便番号を元に住所を取得して、その結果を返却するAPIを作成する。

main.go
package main

import (
    "context"
    "encoding/json"
    "flag"
    "log"
    "net/http"

    "github.com/julienschmidt/httprouter"
    "googlemaps.github.io/maps"
)

type Address struct {
    Prefecture string `json:"prefecture"`
    Locality   string `json:"locality"`
}

type Error struct {
    Message string `json:"message"`
}

var (
    apiKey   = flag.String("key", "APIキー", "API Key for using Google Maps API.")
    language = flag.String("language", "ja", "The language in which to return results.")
    region   = flag.String("region", "JP", "The region code, specified as a ccTLD two-character value.")
)

func main() {
    router := httprouter.New()
    router.GET("/v1/:postalCode", SearchAddressByPostalCode)

    log.Fatal(http.ListenAndServe(":8080", router))
}

func SearchAddressByPostalCode(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    flag.Parse()

    w.Header().Set("Content-Type", "application/json; charset=UTF-8")

    var client *maps.Client
    var err error
    if *apiKey != "" {
        client, err = maps.NewClient(maps.WithAPIKey(*apiKey))
    } else {
        createErrorResponse("APIキーを指定してください", w, http.StatusBadRequest)
        return
    }
    outputFatalLog(err)

    req := &maps.GeocodingRequest{
        Address:  ps.ByName("postalCode"),
        Language: *language,
        Region:   *region,
    }
    resp, err := client.Geocode(context.Background(), req)
    outputFatalLog(err)

    if len(resp) == 0 {
        createErrorResponse("住所が見つかりませんでした", w, http.StatusNotFound)
        return
    }

    address := Address{
        Prefecture: resp[0].AddressComponents[2].LongName,
        Locality:   resp[0].AddressComponents[1].LongName,
    }

    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(address)
}

func createErrorResponse(msg string, w http.ResponseWriter, statusCode int) {
    errorResponse := Error{
        Message: msg,
    }
    w.WriteHeader(statusCode)
    json.NewEncoder(w).Encode(errorResponse)
}

func outputFatalLog(err error) {
    if err != nil {
        log.Fatalf("fatal error: %s", err)
    }
}

go run main.go で起動し、別タブで curl -v --silent localhost:8080/v1/105-0003 | jq のようにアクセスして、以下のようなレスポンスが返ってきていれば OK.

> GET /v1/105-0003 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=UTF-8
< Date: Tue, 30 Jul 2019 07:16:31 GMT
< Content-Length: 47
< 
{ [47 bytes data]
* Connection #0 to host localhost left intact
{
  "prefecture": "東京都",
  "locality": "港区"
}

参考

http://sgykfjsm.github.io/blog/2016/03/13/golang-json-api-tutorial/

おまけ

apiKey = flag.String("key", "APIキー", "API Key for using Google Maps API.")

手元でテストする分には良いが、APIキーをこのままコミットしてしまうと大変なことになるので、環境変数から取得できるように改修する。

.env
GOOGLE_MAP_API_KEY=hogefuga

やり方は色々あるかもしれないが、自分は以下のように書き換えた。

main.go
apiKey := os.Getenv("GOOGLE_MAP_API_KEY")

if apiKey == "" {
    panic("APIキーを指定してください")
    return
}
client, err = maps.NewClient(maps.WithAPIKey(apiKey))

あとがき

今回は Google Maps API をラップしたAPIを自作したことになる。
直接クライアントから Google Maps API を呼んでも問題ないが、わざわざラップするメリットはいくつかある。

  • APIキーの管理が楽
    • 呼び出し元のクライアントが複数になると、APIキーをクライアントそれぞれで管理することになる
  • レスポンスを改造できる
    • {"prefecture_id": "13", "prefecture": "東京都", "locality": "港区" } みたいなことができる
  • Google Maps API のインタフェースが変わったときも安心
    • あまりないと思うが、そのままクライアント側で呼び出すと Google Maps API のレスポンスの形式が変更されたときにバグる可能性がある
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

型アサーション

プログラミング言語Goを読んでいて、型アサーションの説明が分かりづらかったのでメモ

型アサーション

型アサーションはインタフェースの値に使えるインタフェース型の変換のようなものである

io.Writerインタフェース型の値があるとする

var w io.Writer
w = os.Stdout
fmt.Printf("type = %T\n", w) // type = *os.File

この時、wが別のインタフェース型になれるか調べることができる

rw := w.(io.ReadWriter) // 成功する

(*os.File)型はReadおよびWriteのメソッドを持っているため成功する。
rwでは、ReadWriterで定義されているメソッドReadも追加で呼ぶことができる

失敗した場合はpanicが発生するが、成功したかという変数を受け取ればpanicは発生しない

var w io.Writer
w = new(ByteCounter) // ByteCounterは、ReadメソッドがないためReadWriterインタフェースは満たせない
rw := w.(io.ReadWriter) // panicが発生する 
rw, ok = w.(io.ReadWriter) // rwにはゼロ値、okにはfalseが入る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GO言語で簡単なチャット機能の作成

GO言語によるWEBアプリケーション開発

という、上記の書籍を見ながら2章まで進めると簡易的なチャットアプリが作成出来ました。
(とても良い本です!)

まず、1章まで進めると以下のようにさらに簡易的なチャットアプリを作成出来ます。

ezgif.com-video-to-gif.gif

1章では正しいビルドの実行方法や、
並行処理の便利さや
ユニットテストの方法などなど1章だけでも私には十分すぎる内容でした!
1章でもエラーが出て苦戦したり、小一時間悶々とすることも多々ありました・・。
エラー内容としては単純な凡ミスばかりで気付いた時の爽快感を味わいつつ、
2章を進めて行くと、
先ほどのメッセージに加えて送信者の名前も表示されるようになり
他のユーザーと会話することも可能になります。

スクリーンショット 2019-07-30 13.29.03.png
*一つが本名になるので青文字で消してます。

ユーザー名に関しては以下のように認証機能を追加して、
選択できるようにしてあります。

サインイン.png

今回はGoogleアカウントを使用して、
先ほどの画像のようにテスト太郎アカウントと私(青文字)で認証して
メッセージの送受信が行えるようになりました。

複数のタブを開き(今回は2つでしたが)
localhost:8080/login ページでサインインするユーザを選べば
何人とでも複数会話が実行が可能になります。

3章ではプロフィール画像追加について学んでいけるので、
1章・2章で躓いた部分、不明点などを復習してから
次に進んでいきたいと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ぼく 「[]runeを範囲外でスライスしたら当然エラー出るだろうなあ」

ぼく 「あれ?」

Go言語_[]runeのスライス
str := "0123456"
runes := []rune(str)

// 範囲内
fmt.Println(string(runes[0]))   // 0
fmt.Println(string(runes[3]))   // 3
fmt.Println(string(runes[4:6])) // 45
fmt.Println(string(runes[4:7])) // 456

// 範囲外、、のはずだけど
fmt.Println(string(runes[4:8]))   // 456  <= エラー出ないのか、、
fmt.Println(string(runes[10]))    // エラー: index out of range   <= わかる
fmt.Println(string(runes[10:20])) // (空文字が出力)

ぼく 「範囲外のインデックス指定してるんだから、普通 "xxx out of range" 的なエラーが出るでしょ」
ぼく 「Go言語、まさかスライスで範囲外指定してもいいように察してくれるのか、?」
ぼく 「ここ範囲外だからとりあえず空文字返しとこ、的な」
ぼく 「いやいやいや、、」
ぼく 「stringでやってみよ」

Go言語_string配列のスライス
strArray := []string{"0", "1", "2", "3", "4", "5", "6"}
// 範囲内
fmt.Println(strArray[0])  // 0
fmt.Println(strArray[3])  // 3
fmt.Println(strings.Join(strArray[4:6], ""))  // 45
fmt.Println(strings.Join(strArray[4:7], ""))  // 456

// 範囲外
fmt.Println(strings.Join(strArray[4:8], ""))  // エラー: slice bounds out of range
fmt.Println(strArray[10])  // エラー: index out of range
fmt.Println(strings.Join(strArray[10:20], ""))  // エラー: slice bounds out of range

ぼく 「普通にエラー出るな、、」
ぼく 「これならわかる、けどさっきの[]runeの範囲外スライスはなんでエラー出ないんだ」
ぼく 「スライスのスライスだからか、、? []runeは確か配列じゃなくてスライスだったな、(※配列の参照、Goには可変長配列がないので代わりにこれをつかう)」
ぼく 「、、stringスライスのスライスならどうだろ」

Go言語_stringスライスのスライス
var strSlice []string
strSlice = append(strSlice, "0", "1", "2", "3", "4", "5", "6")
fmt.Println(strings.Join(strSlice[4:8], ""))  // エラー: slice bounds out of range
fmt.Println(strSlice[10])  // エラー: index out of range
fmt.Println(strings.Join(strSlice[10:20], "")) // エラー: slice bounds out of range

ぼく 「変わらないな、、」
ぼく 「スライスのスライスは範囲外でもエラー出ない、とかいう話ではないのか」
ぼく 「なんでruneのスライスだけ、範囲外でスライスしてもエラー出ないんだろう」
ぼく 「(わからんtaskete)」

追記(2019/07/31) : 親切な方(aimofさん)降臨

スクリーンショット 2019-07-31 6.52.19.png

ぼく 「神は実在した…(激しく感謝)
ぼく 「aimofさんありがとうございます」
ぼく 「というかcapってなんだ」
ぼく 「キャップだから…帽子か?

調べてみた

  • cap ( capacity ) : 配列の容量
    ▲ 要素数(length)とは異なる

ぼく 「Go言語にはlengthだけじゃなくて、capacityとかあるのか…」
ぼく 「で、[]rune(str)でキャストするときには、なんか暗黙的にcapacity大きく取られてたってことか」
ぼく 「みてみよ」

str := "0123456"
runes := []rune(str)
fmt.Println(cap([]byte(str)))  // 32
fmt.Println(cap(runes))  // 32

// capacity内
fmt.Println(string(runes[4:32])) // 456

// capacity外
fmt.Println(string(runes[4:33])) // slice bounds out of range

ぼく 「ほんとだ、、runeスライスへのキャストで、だいぶ大きめにcapacity取られてるな」
ぼく 「32もある…」
ぼく 「なんでこの数なんだろ」

(下の方で少し試した(おまけ)ことあります)

とりあえずまとめ

  • []runeは範囲外でスライスしてもいいように察してくれる(エラー出ない)
  • それ以外の配列・スライスなら範囲外スライスは普通にエラーがでる
  • 配列・スライスのlen, capの問題だった
  • makeでcapを指定、とかではなく[]rune(str)でキャストすると、capが勝手にある程度の値を取ってくれる
  • len (=length) : 配列の要素数 ( ◁ わかる )
  • cap (=capacity) : 配列の容量 ( ◁ 知らなかった )
  • capの範囲内であれば、例え配列の要素が存在せずとも slice bounds out of range は出ない

補足

  • string: 文字列を示すbyteスライスのイミュータブル参照、Go言語ではstr[0]とか書くと1文字目ではなくひとつ目のbyte値が得られる
  • rune: Unicodeコードポイント、ざっくりいうと文字1つひとつに割り当てられたコード値. runeのスライスはstringに変換できる

追記(2019/07/31) : おまけ

// ▼ 1文字の場合
s := "1"
rs := []rune(s)

// length
fmt.Println(len(rs)) // 1

// capacity ( <= 1文字でもとりあえず32確保する模様)
fmt.Println(cap([]byte(s))) // 32
fmt.Println(cap(rs))        // 32


// ▼ 32文字の場合
st := "12345678901234567890123456789012"
rus := []rune(st)

// length
fmt.Println(len(rus)) // 32

// capacity ( <= 32文字なら、まだcapacityも32のまま)
fmt.Println(cap([]byte(st))) // 32
fmt.Println(cap(rus))        // 32


// ▼ 33文字の場合
str := "123456789012345678901234567890123"
runes := []rune(str)

// length
fmt.Println(len(runes)) // 33

// capacity ( <= 32を超えて初めて増加! )
fmt.Println(cap([]byte(str))) // 48
fmt.Println(cap(runes))       // 36
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go初学メモ

単なるメモです。まとまってきたら良い感じにまとめて記事にします。

インタフェース

  • goにはOSとの緩衝材になってくれるインタフェースという概念がある→わかる
  • インタフェースがあることで、OS側のファイルディスクリプタ(メモリ番地的な、アドレス的なもの)を指定しなくても良くなっている
  • インタフェースは、構造体などの具象型が持つべきメソッドを表現している
    • クラスのような概念も含まれている?
    • ということは構造体 = インスタンス

構造体

  • 構造体はインタフェースで定義されているメソッドを持つ
    • 具体的なメソッド内容の記述は構造体の中で行う
    • ということはインタフェースはあくまで構造体へメソッドをマッピングするためにあるような立ち位置?
    • 構造体とインタフェースの関係性はN:1? N:N?
  • メソッドの定義方法は↓
main.go
func (r Receiver) MethodName {
  'メソッドにやらせたい処理'
}
  • 全体的に 変数名 型 みたいな定義をしているっぽい。型 変数名 で定義してきた自分(よわよわUnityマン)にはびっくり
  • 構造体をそのまま実行するわけではなくて、インタフェースに書かれた(なんて言うんだろう、定義とは言わない気がする)メソッドを構造体で具体的に書いてあげて、それをmainのfunctionの中でimportというか宣言してあげて、実行するみたいな手続きを踏んでるくさい
  • インタフェースでOSとの橋渡しを、構造体で実際のfunctionとの橋渡しをしているみたいな役割分担?
  • たしかにシステムコールとかするならこの構成が柔軟な気がする

他言語で言うクラスはインタフェースなのか?構造体なのか?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む