emahiro/b.log

勉強記録と書評とたまに長めの呟きを書きます

goで任意引数でtemplateでの表示を変えたりしたい場合の考察

golangで関数に任意引数を取り、その任意引数にmapを指定することで、template場でmapで定義したinterfaceを見て表示を変えたいという実装を考えます。

やりたいこと

以下のようなことをしたいと想定します。

func RendarHTML (flags ...map["string"]interface{}){
  // flags に各種表示を分けるstatusを入れてtemplate側で制御する
  
}

golangにおける任意引数とは?

golangでは、LL言語はじめとしたオブジェクト指向言語にあるようなデフォルト引数という機能を使うことができません。
rubyとかだと

def renderHtml(templ, flag=false)
  # flagを使って何かする
end

# callする側でflagを使わないとき
renderHtml("index.html")

# flagの値を使うとき
renderHtml("detail.html", true)

上記のようにmethodに対してdefatult引数を指定して、その引数はcallする側で指定されなければdefatult引数に与えられた値がmethod内部で使用されるということが可能です。

しかし、golangではこのdefault引数を取ることはできません。
また、静的言語のため、指定した引数は必ず呼び出し側で関数にsetすることが求められます。
setしないとコンパイルが通りません。

// flagでfalseをデフォルトで指定するようなことはできない
func renderHTML(tmpl string, flag bool){ 
  // 何かしらの処理
}

// 呼び出す側

// ◯ コンパイルが通るとき
renderHTML("index.tmpl", false)

// × コンパイルが通らない
renderHTML("detail.tmpl")

// 関数に指定されている引数は満たさなければならない。

ここでgolangで使うのが 任意引数。 言葉通り、呼び出す側でsetしてもいいし、しなくてもいい任意の引数をgolangでは関数に使うことができます。
可変引数が正しい呼称らしいですね

qiita.com

任意引数を使った実装パターンの検討

まず任意引数の仕様として 任意引数をとる場合は必ずsliceとして扱う というのがあります。

func renderHTML(tmpl string, flags ...bool){
    // flagsはslice型。この場合では []book となっている。
}

// 引数をとる
renderHTML("index.tmpl", true)

// 引数をとらない
renderHTML("detail.tmpl")

// どちらでもコンパイルは正常に通ります。

しかし、引数一つでも任意引数をとっている関数内ではsliceから特定の値を取り出さないと一番最初に意図していたようなflagによってtemplateの表示を変更するような実装はできないです。 上記の例では []bool の中からお目当のstatusを持っているかを判別するような実装をすることはしません。

ここで実際に具体的な実装方法を検討します。

任意引数は関数内ではsliceとして扱われるので、flagsという直接boolを扱うことを連想させるような引数をやめて、mapをとります。
このとき関数内の変数名は、追加分なので extra のような変数名にしておくと可読性が高くなると思います。

sample実装

具体的な実装方法として、以下のようなmethodを最初考えます。

fumc renderHTML (tmpl string, extra ...map["string"]interface{}){
  // extraを使って描画する
  // extras は []map(string)interface{}
  
  RenderPage(ctx, "templete/path.tmpl",{"Extra":extra})
}

// こんな感じの引数にする
renderHTML("index.tmpl", map["string"]interface{}{"isIOS": true, "isAndroid": false})

// 引数をとらないことも可能
renderHTML("detail.tmpl")

※ 関数名は適当。

renderHTML内の引数に任意引数を使うことで、メソッドの汎用性が高くなります。
何か余分なデータや追加でデータを持たせないときはextrasに全て突っ込んで仕舞えば問題ないです。

これをtemplate側で使うにはいくつか方法があります。

  • templateでsliceのelementを扱う関数を使う
    • range
    • index
<!-- rangeで一つずつ取り出してloopを回す -->
{{ $ext := range $.Extra }}
{{ end }}

<!-- 到底のindex位置の取得する -->
{{ $ext := index $.Extra 3 }}

しかし、extraがそのまま渡されてきてtemplate側で条件分岐を使ったり、indexを制御したりするのはあまり筋がいいとは言えません。
何より、Extraに紐づく値がなければtemplateのコンパイルエラーが起きる場合もあります。

よりベターな実装方法の検討

任意のsliceで返されるextra引数をそのままとるのではなく、ルールを決めます。

  • 任意引数でとる値は必ず一つ
  • 関数側でslice型になっても、slice型のindex:0が必ずflagsをとることが決まっていれば問題ありません。
  • privateなinterfal関数を用意する

以下のような実装になります。

func renderHTML(tmpl string, extra ...map[string]interface{}){
    ext := extra[0] //先頭に来ることが確定済み
    internalRenderHTML("index.tmpl", ext) 
}

func internalRenderHTML(tmpl string, extra map[string]interface{}){
    // extraを使ってstatusを取り出す
}

任意引数のindex0番が使いたいflagなにで以下のような風に呼び出し側で使います。

renderHTML("index.tmpl", map["string"]interface{}{"IsIOS": true, "IsAndroid": false})

html側で利用するときはindexやreangeの関数を使わずとも、extraはslice型なくmapで受け取れるようにします。

{{ /* iOSモードもとき */ }}
{{ if $.Extra.IsIOS }}
{{ end }}

{{ /* Androidモードのとき */ }}
{{ if $.Extra.IsAndroid }}
{{ end }}

こういった感じでinternalで共通のmethodを呼ぶことで環境ごとにtemplateで使えるようにします。変なelement管理等は必要ないです。

任意引数でslice型をそのまま使うより単体として扱った方が使い勝手がいいですし、templateでもすぐにカンマ区切りで呼び出せるので便利だと思いました。