2013年4月8日月曜日

Go言語でWebサーバを立てる 補足編


こんにちは。scarvizです。

今回は「Go言語でWebサーバを立てる」の記事を書いているときや、AZサーチ(http://gosrv.scarviz.net/azsearch)を作成しているときに色々試していたことを紹介します。




■http.RequestのFormValue関数について
受け取った文字列に「;」が入っていると、それ以降を除去する
 ※URLエンコードをして送った場合はそのままの状態で取り出されます。
 →(4/9追記)訂正。下記に追記しています。
URLエンコードされた文字列は自動的にデコードされる

例えば、以下のようにパラメータとしてtestに「test;mes」を、test2に「test;mes」をURLエンコードした「test%3bmes」を設定して渡したとします。

package main

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

func main() {
 url, _ := url.Parse("http://localhost/testpage?test=test;mes&test2=test%3bmes")
 var r http.Request
 r.URL = url
 var w http.ResponseWriter
 Test(w, &r)
}

func Test(w http.ResponseWriter, r *http.Request) {
 fmt.Println("url:", r.URL)

 // 設定された文字列を取得
 test := r.FormValue("test")
 test2 := r.FormValue("test2")

 fmt.Println("test:", test)
 fmt.Println("test2:", test2)
}

結果:
url: http://localhost/testpage?test=test;mes&test2=test%3bmes
test: test
test2: test;mes
 

testの「test;mes」がFormValue関数を通ると「test」までしか取得されていません。
また、URLエンコードをしているtest2の文字列がFormValue関数を通ると自動的にデコードされています。

→(4/9追記)
「;」はセパレータなので、除去されたのではなく、上記ならmesがパラメータ扱いになっただけでした。


■レスポンスヘッダー情報にContent-typeのtext/htmlを設定するとIEでうまく表示されない
http.ResponseWriterのHeader().Add("Content-type", "text/html charset=utf-8")として、htmlファイルを表示するようにした場合、IE(IE9で確認)ではhtmlファイルをダウンロード(ファイルのダウンロードメッセージが表示される)する動きになっていました。
例えば以下のようにしている場合、test.htmlファイルをダウンロードしようとします。

func Test(w http.ResponseWriter, r *http.Request) {
 // IEに対応させるには、このようにレスポンスヘッダーを設定してはいけない
 w.Header().Add("Content-type", "text/html charset=utf-8")

 t, _ := template.ParseFiles("test.html")
 t.Execute(w, t)
}

レスポンスヘッダー情報を設定せずに返した場合、問題なくtest.htmlが表示されるようになりました。

→(4/9追記)
「"text/html charset=utf-8"」の箇所に誤りがあって、「"text/html; charset=utf-8"」とすればちゃんとIEでも表示されました。


■JSONの扱い
JSONの値で"null"が渡される場合について
http.GetなどでレスポンスとしてJSONを受け取る場合、値に"null"が格納されていることがあります。
string型の値を扱うキーでnullだったり、int型の値を扱うキーでnullだった場合、どういう動きをするか検証してみました。

package main

import (
 "encoding/json"
 "fmt"
 "strings"
 "net/http"
 "io/ioutil"
)

type Mes struct {
 TestStr string
 TestInt int
}

func main() {
 // nullが値にある
 jsonStr := `{"teststr":null,"testint":null}`
 fmt.Println(jsonStr)
 
 // http.GetなどでJSONを受け取ったと仮定して、レスポンスのBodyに格納
 response := http.Response{Body:ioutil.NopCloser(strings.NewReader(jsonStr))}
 
 // レスポンスのBodyを読み込む
 data, _ := ioutil.ReadAll(response.Body)
 var mes Mes
 // JSONから構造体へ
 json.Unmarshal(data, &mes)
 fmt.Println(mes)
}

結果:
{"teststr":null,"testint":null}
{ 0}

ゼロ値に変換されています。問題なさそうですね。
→(4/9追記)訂正。下記に追記しています。
しかし、実際にhttp.GetでJSONを結果として受け取り、Unmarshalをすると以下のエラーが出力されました。

json: cannot unmarshal null into Go value of type string
json: cannot unmarshal string into Go value of type int

検証方法が悪いのか、Getで返って来る時に何か別の要因があるのかまでは分かっていません。
とりあえず僕は、string型なら""(空文字)に、int型なら0というようにゼロ値に置換して対応しました。

→(4/9追記)
検証方法に誤りがありました。エラーを拾っていないので、当然エラーは出力されないというだけでした。
以下のようにエラーを取得し、出力するようにした場合、今回遭遇したnullのエラーが表示されました。

package main

import (
 "encoding/json"
 "fmt"
 "io/ioutil"
 "net/http"
 "strings"
)

type Mes struct {
 TestStr string
 TestInt int
}

func main() {
 // nullが値にある
 jsonStr := `{"teststr":null,"testint":null}`
 fmt.Println(jsonStr)

 // http.GetなどでJSONを受け取ったと仮定して、レスポンスのBodyに格納
 response := http.Response{Body: ioutil.NopCloser(strings.NewReader(jsonStr))}

 // レスポンスのBodyを読み込む
 data, _ := ioutil.ReadAll(response.Body)
 
 var mes Mes
 // JSONから構造体へ
 err := json.Unmarshal(data, &mes)
 
 if err != nil {
  fmt.Println(err)
  return
 }
 
 fmt.Println(mes)
}

結果:
{"teststr":null,"testint":null}
json: cannot unmarshal null into Go value of type string


あとはinterfaceを使えば、どんな値でも問題ないので、こちらの方が一般的のようです。
interfaceを使う例は以下になります。

package main

import (
 "encoding/json"
 "fmt"
 "strings"
 "net/http"
 "io/ioutil"
)

func main() {
 // nullが値にある
 jsonStr := `{"teststr":null,"testint":null}`
 fmt.Println(jsonStr)
 
 // http.GetなどでJSONを受け取ったと仮定して、レスポンスのBodyに格納
 response := http.Response{Body:ioutil.NopCloser(strings.NewReader(jsonStr))}
 
 // レスポンスのBodyを読み込む
 data, _ := ioutil.ReadAll(response.Body)
 
 // JSONをinterfaceのMapに格納する
 var mes map[string]interface{}
 json.Unmarshal(data, &mes)
 fmt.Println(mes)
}

結果:
{"teststr":null,"testint":null}
map[testint:<nil> teststr:<nil>]

→(4/9追記)
ポインタを使うことでnullを取ることも出来るそうです。
ご指摘頂いた方が作成されたものを共有致します。
http://play.golang.org/p/G_4TOY1RT-


JSONでリクエストする場合
JSONに変換するため用意する構造体は、エクスポートしている必要があるので、大文字から始まる名前で定義すると思います。
しかし、受け取り側が大文字を考慮していない場合、定義されていないキーとして、エラーコードが返って来る場合があります。
この場合は、Marshal関数でJSON形式に変換した後、リクエストで受け付けるJSONキー名とあわせるように、大文字を小文字に置換する必要があるようです。

package main

import (
 "encoding/json"
 "fmt"
 "os"
 "strings"
)

type Mes struct {
 TestStr string
 TestInt int
}

func main() {
 // 構造体
 str := Mes{TestStr: "Test", TestInt: 10}
 fmt.Println(str)

 // 置換用
 r := strings.NewReplacer(`"TestStr":`, `"teststr":`, `"TestInt":`, `"testint":`)

 // 構造体からJSONにする
 jsonStr, _ := json.Marshal(str)
 // キーに大文字が存在する
 os.Stdout.Write(jsonStr)
 fmt.Println("") // 改行

 // キーだけすべて小文字にする
 jsonStr = []byte(r.Replace(string(jsonStr)))
 os.Stdout.Write(jsonStr)
}

結果:
{TestStr 10}
{"TestStr":"TestStr","TestInt":10}
{"teststr":"TestStr","testint":10}

ちょっと面倒な感じがします。すべてのサービスで大文字がダメというわけではないですし。
※ちなみにGCMサーバは大文字を受け付けてくれません。
 ここはぜひGoに置き換えて欲しいところですね!

→(4/8追記)
構造体を以下のようにすることで、別名として扱えるそうです。
type Mes struct {
 TestStr string `json:"teststr"`
 TestInt int `json:"testint"`
}
やしさん(http://kwmt27.net/)に教えて頂きました!
ありがとうございます!



■GoのWebサーバを使う場合
GoのWebサーバを使う場合、一つ実施しないといけないことがあります。
まずは下記を実行してください。

service httpd status

もし、実行中になっていたら、下記を実行してください。

service httpd stop

httpdはあなたにはきっと不要なサービスですよ(たぶん)。
※少なくとも、80番ポートをGoWebサーバで使用したい場合は、80番ポートを使用しているhttpdを停止しないといけません。


みなさんのGoWebサーバに、これらがお役に立てば幸いです。

3 件のコメント:

  1. 「JSONの扱い」の②に追記したものに加え、下記項目にご指摘頂いたので、追記致しました。

    ・「http.RequestのFormValue関数について」の①
    ・「レスポンスヘッダー情報にContent-typeのtext/htmlを設定するとIEでうまく表示されない」
    ・「JSONの扱い」の①

    すごく勉強になりました!
    ありがとうございます!

    色々試して悩んでいたので、一瞬で解決してちょっと感動です。

    返信削除
  2. 構造体のフィールドのタグ(`json:"teststr"`とか)ですが、別名というより付加情報に近いです。複数設定できますし、reflectパッケージで型情報から取得できます。JSONだけではなく、xmlやMessagePackなどでリフレクションを使って、シリアライズ/デシリアライズを行なう際などに使われるようです。

    返信削除
    返信
    1. なるほど。C#の属性みたいな感じですね。
      ありがとうございます!

      削除