【Golang】了解Golang中的接口(interface)的小窍门很有益处

简而言之

    • 去年の8月に社会人エンジニアになり、今年の2月までAPIサーバーの開発をRuby on Railsで行なっていた新卒Rubyistです。

 

    • 3月にサーバーサイドを全てGolangで行う会社に転職しました。今までRailsでの業務が多かったため、interface{}という概念がどうしても身につきませんでしたが、ある程度業務に慣れ、戦い続けて学んだことをまとめようと思いました。

 

    Golangのinterfaceについては知見は多くあるのですが、今年はアウトプットを大事にしていきたいという目標から、記事にまとめました。

在开始阅读之前往下看

    • Golangのインタフェースは型の1つです。

string や int64 が関数を持っているように、インタフェースで型を宣言してその型に関数を持たせることができます。

構造体がインタフェースで宣言されているGetRadius関数を持つと、この構造体の型はCircleになります。

type(
     Circle interface{
          GetRadius() int64
     }

     CircleImpl struct{}
)

// NewCircle が `GetRaidus`関数を持っているため、型は`Circle`になる。
func NewCircle() *CircleImpl {
     return &CircleImpl
}

func (*CircleImpl) GetRadius() int64{
     return int64(1)
}

熟悉Golang的人可能认为这是理所当然的,但对于不使用interface{}概念的Ruby开发者来说,理解和不理解上述要点之间存在很大差异,因此我将其写在文章的最上方。基于这些,我认为深入理解interface{}很重要。

继续战斗我学到的教训

1. 不管是什么类型的,都能接纳的杂货店。

    Golangのinterfaceではどんな型でも受け取れることができます。
package main
func main() {
  intValue := int64(10) 
  strValue := "go interface"
  PrintAnyType(intValue) // => 10
  PrintAnyType(strValue) // => "go interface" 

  var interface1 interface{} = "interface1"
  fmt.Println(interface1) // => interface1
  interface1 = uint8(2)
  fmt.Println(interface1) // => 2 
}
// PrintAnyType print any type of variable.
// 引数の型がinterface{}であるために、どんな型の変数でもPrintAnyType関数は受け入れてくれる。
func PrintAnyType(variable interface{}){
  fmt.Println(variable)
}
    • なんでこれを書こうと思ったかというと、今まで動的言語でのAPIサーバーの開発が多かったため、「型が存在する言語でJSONを返すときの処理ってどうなっているだろ?」と思ったところ、気になって会社のコードを調べて、下のコードを見つけたのがきっかけでした。

 

    下のようにマップのキーは string だけど、 値はinterface{} になっているので、いちいち値の型をすべて1つの型に揃える必要がなく、JSONを作ることができるのかと納得しました。(会社では、echoのフレームワークを使っています。)
func JSONHTTPSuccessHandler(data interface{}, c echo.Context) error {
    return c.JSON(http.StatusOK, data)
}

当一个结构体具有接口类型时,原本的类型信息、结构体的方法和字段将会消失。

    • 上の1みたいに、interface{}型で宣言されている変数に値を代入して、型に沿った通りの動きをしてくれると思ったところ、下のようなコードを書いたときに、コンパイルが通らないってことがあります。

 

    • なぜコンパイルが通らないかというと、strSliceInterfaceはJoin関数の引数として渡せれるときに型がinterface{}となっているからです。

 

    そのため、strings.Joinが期待している []string{} とはならず、コンパイルエラーが発生してしまいます。
package main

import (
    "fmt"
    "strings"
)

func main() {
    var strSliceInterface interface{}
    strSliceInterface = []string{"hoge", "fuga"}
    fmt.Println(strings.Join(strSliceInterface, ", ")) 
    // => cannot use strSliceInterface (type interface {}) as type []string in argument to strings.Join: need type assertion
    // => エラー文が言っているように、strSliceInterfaceの型は`interface{}`となっているため、期待している型が渡されていない。
}
     interface{}がなんでも屋さんであるがゆえ、ただ代入するときはコンパイルが通ると思いますが、実際に使用するとなったときは、型がinterface{}になっているので、気をつけましょう。

将一个变量赋值为interface{},以至于丢失/没有下面类型的信息,然后将该变量恢复为原来的类型的方法是什么?

    • 2.のように、変数に代入するときはinterface{} を使ってしまったけれど、interface{}として代入する前の型を取り戻したいというときには、変数.(型)と書くことで型の情報を取り戻すことができます。

上のinterface4.goに、 strSlice := strSliceInterface.([]string) と加えることでstrSliceに[]stringの型を渡せます。

これを Type Assertion と言います。
型変換が上手くいったかどうかをしっかり調べるために、ok(bool)が true になっているかをしっかり確認しましょう。

package main
import (
    "fmt"
    "strings"
)
func main() {
    var strSliceInterface interface{}
    strSliceInterface = []string{"hoge", "fuga"}
    strSlice, ok := strSliceInterface.([]string) // => stringのスライスに戻している。 
    fmt.Println(ok) // => true ここが falseのときは、type assertionは失敗している。
    if ok {
        fmt.Println("type assertion succeeded.")
    }
    fmt.Println(strings.Join(strSlice, ", ")) // => hoge, fuga
}

应用

    • 上のType Assertionの応用がTour of Goにあるため、それを参考にします。

switch + interface{}.(type) で期待している型が渡ってきたときに特定の処理を実行することができます。

package main

import "fmt"

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
}

func main() {
    do(21)
    do("hello")
    do(true)
    // => Twice 21 is 42
    // => "hello" is 5 bytes long
    // => I don't know about type bool!
}

可以为不具有共通点的多个结构体添加新类型。

interface を利用することで、Golangでダックタイピングをすることができます。
ダックタイピングについては知見が多くあるので、詳しくは書きませんが、interface{} を使うことで複数の構造体を同じ1つの型に変換します。

ads 変数に代入する前に構造体が必要な関数を持っているのかをエディタが教えてくれるため、コンパイルを実行する前の関数の追加漏れがなくなりました。

interface{}がないRubyだと、そのオブジェクトに必要な関数があるかどうかは実行しないとわからないため、インタフェースで先に約束が決められると、先読みしながらコードをかけるので便利だと感じました。

在之前

package main

import "fmt"

type (
    // AdVideo ad video response struct
    AdVideo struct {
        VideoURL string `json:"video_url"`
        AdType   int64  `json:"ad_type"`
    }
    // AdPoster ad poster response struct
    AdPoster struct {
        PosterURL string `json:"poster_url"`
        AdType    int64  `json:"ad_type"`
    }
)

func main() {
    var ads []interface{}
    ads = append(ads, NewAdVideo())
    ads = append(ads, NewAdPoster())
    fmt.Println(ads) // => [{video url 1} {poster url 3}]
}

func NewAdVideo() AdVideo {
    return AdVideo{VideoURL: "video url", AdType: int64(1)}
}

func NewAdPoster() AdPoster {
    return AdPoster{PosterURL: "poster url", AdType: int64(3)}
}

完成

package main

import "fmt"

type (
    // Adインタフェース
    Ad interface {
        GetAdType() int64
    }
    Ads []Ad
    // AdVideo ad video response struct
    AdVideo struct {
        VideoURL string `json:"video_url"`
        AdType   int64  `json:"ad_type"`
    }
    // AdPoster ad poster response struct
    AdPoster struct {
        PosterURL string `json:"poster_url"`
        AdType    int64  `json:"ad_type"`
    }
)

func main() {
    var ads Ads
    ads = append(ads, NewAdVideo())
    ads = append(ads, NewAdPoster())
    for _, ad := range ads {
        fmt.Println(ad)
    }
}

func NewAdVideo() AdVideo {
    return AdVideo{VideoURL: "video url", AdType: int64(1)}
}

// => Adのインタフェース型を持つために、`GetAdType`を実装した。
func (v AdVideo) GetAdType() int64 {
    return v.AdType
}

func NewAdPoster() AdPoster {
    return AdPoster{PosterURL: "poster url", AdType: int64(3)}
}

// => Adのインタフェース型を持つために、`GetAdType`を実装した。
func (p AdPoster) GetAdType() int64 {
    return p.AdType
}

将interface{}嵌入结构体中

    • こちらのinterface{}の使い方も記事を書く際に多くの知見があると知ったので特にここでは細かく書くことはしません。

 

    • PHPやJavaでは、implements を使ってinterface を取り込みますが、Golangではフィールドがinterface{}の型を持つようにして、interface{}を取り組むという言語仕様になっています。

 

    • この言語仕様を理解したときに便利だなと感じました。

例えば、新しいAPI実装で新たに構造体を作成するときに、必要なメソッドだけ揃っていたら正常に動くため、とりあえずinterface{}で宣言された関数を新しい構造体に実装すればいいだけと感じました。
上の4と似ていますが、やることリストが最初から決まっていて、リストにある関数を定義することでやることが完了していく感覚になったので、わかりやすいなと感じました。

package main

import "fmt"

type (
    // TagClient tag clientインタフェース
    TagClient interface {
        GetTag()
    }

    // TagClientImpl TagClientの実際の処理
    TagClientImpl struct {
    }

    // TagClientImplV2 TagClientの実際の処理
    TagClientImplV2 struct {
    }

    // ArticleClientImpl Tagという名前で、TagClientインタフェースを持つ。
    ArticleClientImpl struct {
        Tag TagClient
    }
)

func main() {
    articleClientImpl := NewArticleClientImpl()
    // 記事のタグを取得するには、`Tag`のインタフェースを経由して、`GetTag`を呼び出す。
    articleClientImpl.Tag.GetTag()
}

func NewArticleClientImpl() *ArticleClientImpl {
    // Tagフィールドに渡すものをNewTag()からNewTagV2()に変更するだけ。簡単...!!
    return &ArticleClientImpl{Tag: NewTagV2()}
}

func NewTag() TagClient {
    return &TagClientImpl{}
}

func (c *TagClientImpl) GetTag() {
    fmt.Println("Here is article tag")
}

func NewTagV2() TagClient {
    return &TagClientImplV2{}
}

func (c *TagClientImplV2) GetTag() {
    fmt.Println("Here is article tag V2")
}

赠品 – 自定义错误的制作方法

    golangのerrorってどんな中身なのかを調べようと思い、ドキュメンテーションを読んだところ、なんとinterface{}でした。
type error interface {
    Error() string
}
    • ということは、構造体にError() string関数を持たせれば、その構造体をerror型として扱ってくれるのか…?

実験で書いてみたら、なんと自前で用意した構造体が見事error型となりました…!

Error() stringさえあれば、error型になるので、いろいろな場面のエラー構造体が作成できると思います。

package main

import (
    "fmt"
    "time"
)

type (
    Article struct {
        postedAt time.Time
    }
    // カスタムエラー構造体
    ArticleNotFoundError struct {
        Message string
    }
)

func main() {
    article, err := GetArticle()
    if err != nil {
        fmt.Println(err.Error()) // => "article not found"
    }
}

func GetArticle() (*Article, error) {
    return nil, NewArticleNotFoundError("article not found")
}

func NewArticleNotFoundError(message string) *ArticleNotFoundError {
    return &ArticleNotFoundError{Message: message}
}

// このメソッドがあるおかげで、ArticleNotFoundError構造体の型はerrorになり、GetArticleの第2引数の型はerrorになりました。
func (e *ArticleNotFoundError) Error() string {
    return e.Message
}

总结

    • 長くなりましたが、この記事を短くまとめると、golangでの interface{} は型である になります。

転職する前にgolangのinterface{}のことを勉強しておきましたが、実際に業務に入って書いてみると理解できていないということもあり、多く戦わないといけなくなりました。
(PHPやJavaには軽く触れていて、インターフェースという概念を全く知らないわけではありませんでしたが、いまいち「これを使う必要はあるのか…?」と腑に落ちていないことが多かったです…。)

業務で触っているみると、便利なものだと感じたので、戦って生き延びたかいがあったと思いました。これからも適材適所で使っていきたいと思います。
GWで時間ができたため学んだことをまとめましたが、今後新たに学ぶことがあったら、追記していきます。

广告
将在 10 秒后关闭
bannerAds