こんにちは、イサムです!
本記事では、前回に引き続きGo言語の基本文法についてまとめます。
- Go言語の基本文法について知りたい!
- Go言語の基本文法についてのまとめ
メソッド
メソッドとは、構造体などの型の変数(レシーバー変数)を引数として持った、型に埋め込む関数のことになります。
構造体だけでなく、インターフェース型以外のどんな型にでもメソッドを埋め込むことができます。
Go言語には、クラスという概念は存在しませんが、型にメソッドを埋め込むことでクラスに似た振る舞いをすることができます。
メソッドの定義
func (レシーバ変数) 関数名(引数) 戻り値の型 {}で定義することできます。
同じレシーバー変数で複数メソッドの定義や定義したメソッドから別のメソッドを呼び出すこともできます。
メソッドとレシーバー変数で使用する型が同じパッケージにある必要があります。
構造体にメソッドを定義した場合、構造体のフィールドやメソッドのように呼び出すことができます。
type User struct {
id int
name string
}
// 構造体の型のレシーバー変数を持ったメソッドの定義
func (u User) ShowUser() {
fmt.Println(u.id, u.name)
}
func main() {
u := User{1, "Osamu"}
u.ShowUser()
}
ポインタレシーバー
レシーバー変数にポインタを使用することで、構造体のフィールドの値を変更することができます。
メソッドには一般的にポインタレシーバーを使用します。(値レシーバーとポインタレシーバーは混在させないでどちらかに統一するのが望ましいです。)
- メソッドがレシーバが指す先の変数を変更するため
- メソッドの呼び出しごとに変数のコピーを避けるため
ただし、メソッドの呼び出し側は、値型で定義しても、ポインタ型のレシーバーを持つメソッドを呼び出すことができます。
type User struct {
id int
name string
}
func (u *User) ShowUser() {
fmt.Println(u.id, u.name)
}
// ポインタレシーバーで構造体の値を変更
func (u *User) SetUserName(name string) {
u.name = name
}
func main() {
u := &User{1, "Osamu"}
u.ShowUser() // 1, Osamu
u.SetUserName("Osamu2")
u.ShowUser() // 1, Osamu2
}
メソッドの実態
メソッドの実態は、関数の第1引数にレシーバ変数、それ以降に引数を持つ関数になります。
// コンパイル前
func (u *User) SetUserName(name string) {
u.name = name
}
// コンパイル後
func SetUserName (u *User, name string) {
u.name = name
}
インターフェース
インターフェースは、メソッドの集まりになります。
インターフェースでは各メソッドの具体的な実装はせず(抽象化)、インターフェースを実装する型が具体的な処理を実装し、異なる型同士で共通の性質を持たせることができます。
インターフェースを実装するときには、他の言語で使用されるimplementsキーワードは使用しません。
ある型TがインターフェースIで定義したメソッドを全て実装している場合、型Tは、インターフェースI型として振舞うことができます。型TがインターフェースI型として振舞うとは、型TがI型の変数に代入したり、引数として関数に渡すことができるということです。
インターフェースの定義方法
type インターフェース名 interface {メソッド1名(引数の型, …) (戻り値の型, …), メソッド2名(引数の型, …) (戻り値の型, …), … }で定義することができます。
type Name interface {
ShowFirstName()
ShowLastName()
}
インターフェースの埋め込み
インターフェースにインターフェースを定義することができます。
// name, age インターフェースを埋め込む
type People interface {
name
age
}
type name interface {
ShowFirstName()
ShowLastName()
}
type age interface {
ShowAge()
}
構造体にメソッドを埋め込んでインターフェースを実装
構造体にインターフェースを実装することができます。
インターフェースに定義されているメソッドを全て実装することで、インターフェースの型として振舞うことができます。
今回は、構造体に言及していますが、インターフェース型でなければ、どんな型でもインターフェースを実装することができます。
構造体にインターフェースを実装する方法
func (レシーバ変数 構造体名) メソッド名 {…}で構造体にインターフェースを実装することができます。
構造体にメソッドを定義するの方法と変わりません。
// インターフェースを定義
type Name interface {
ShowFirstName()
ShowLastName()
}
// 構造体を定義
type person struct {
firstName string
lastName string
}
// 構造体の初期化関数を定義
func NewPerson(firstName string, lastName string) *person{
return &person{firstName , lastName}
}
// 構造体personにインターフェースNameを実装
func (p *person) ShowFirstName(){
fmt.Println(p.firstName)
}
// 構造体personにインターフェースNameを実装
func (p *person) ShowLastName(){
fmt.Println(p.lastName)
}
func main() {
p := NewPerson("Osamu", "Tanaka")
p.ShowFirstName()
p.ShowLastName()
}
インターフェースの部分実装
インターフェースで定義された一部のメソッドを埋め込んだ構造体を、その他のメソッドを埋め込んだ構造体に埋め込むことで、インタフェースを実装させることが可能です。
// インターフェースを定義
type People interface {
ShowName()
ShowGender()
}
// 構造体を定義
type person struct {
firstName string
lastName string
}
// 構造体personにインターフェースPeopleの一部を実装
func (p *person) ShowName(){
fmt.Println(p.firstName, p.lastName)
}
type Gender int
// iotaで連番を生成
const (
Male = iota
Female
)
// 構造体を定義(インターフェースPeopleの一部を実装した構造体personを埋め込む)
type male struct {
*person // 変数名込みの定義だとShowNameが未定義と判断される(変数名を付与するとメソッドを呼び出すときに、構造体.変数名.メソッド名なので認識されない)
gender Gender
}
// 構造体maleに構造体personでインターフェースの未実装メソッドを実装
func (m *male) ShowGender(){
fmt.Println(m.gender)
}
// 構造体を定義(インターフェースPeopleの一部を実装した構造体personを埋め込む)
type female struct {
*person // 変数名込みの定義だとShowNameが未定義と判断される(変数名を付与するとメソッドを呼び出すときに、構造体.変数名.メソッド名なので認識されない)
gender Gender
}
// 構造体maleに構造体personでインターフェースの未実装メソッドを実装
func (f *female) ShowGender(){
fmt.Println(f.gender)
}
// 構造体の初期化関数を定義
func NewPeople(gender Gender, firstName string, lastName string) People{
p := &person{firstName , lastName}
switch gender {
case Male:
return &male{p, gender}
case Female:
return &female{p, gender}
default:
return &male{p, gender}
}
}
func main() {
p := NewPeople(Male, "Osamu", "Tanaka")
p.ShowName()
p.ShowGender()
}
構造体personは、インターフェースPeopleの一部のメソッドしかもっていないため、インターフェースPeopleを実装していることにはなりません。
しかし、構造体personで未実装のメソッドを実装した構造体male, femaleに構造体personを埋め込むことで、インターフェースPeopleを実装していることになります。
インターフェースを利用するメリット
インターフェースを利用するメリットとして、各型毎にメソッドを定義しなくて良いことです。これにより、コードの冗長化を防ぎ、コード自体もスッキリできます。
(今回の例は、簡単な実装のため、効果は薄い。)
インターフェースなし
// 構造体①を定義
type Person struct {
name string
age int
}
// 構造体②を定義
type Dog struct {
name string
age int
}
// 構造体①の初期化関数を定義
func NewPerson(name string, age int) *Person{
return &Person{name: name, age: age}
}
// 構造体②の初期化関数を定義
func NewDog(name string, age int) *Dog{
return &Dog{name: name, age: age}
}
// 構造体①にインターフェースAnimalを実装
func (p *Person) ShowInfo(){
fmt.Println(p.name)
fmt.Println(p.age)
}
// 構造体②にインターフェースAnimalを実装
func (d *Dog) ShowInfo(){
fmt.Println(d.name)
fmt.Println(d.age)
}
// ②と構造体が異なるだけで、同じ処理でコードが冗長 ・・・①
func ShowInfoPerson(p *Person) {
p.ShowInfo()
}
// ①と構造体が異なるだけで、同じ処理でコードが冗長 ・・・①
func ShowInfoDog(d *Dog) {
d.ShowInfo()
}
func main() {
p := NewPerson("Osamu", 20)
d := NewDog("Pochi", 10)
ShowInfoPerson(p)
ShowInfoDog(d)
}
インターフェースあり
// インターフェースを定義
type Animal interface {
ShowInfo()
}
/*
各構造体に実装したメソッドを呼び出す
インターフェースなしで冗長だったコードを統一
*/
func ShowInfo(s Animal){
s.ShowInfo()
}
// 構造体①を定義
type Person struct {
name string
age int
}
// 構造体②を定義
type Dog struct {
name string
age int
}
// 構造体①の初期化関数を定義
func NewPerson(name string, age int) *Person{
return &Person{name: name, age: age}
}
// 構造体②の初期化関数を定義
func NewDog(name string, age int) *Dog{
return &Dog{name: name, age: age}
}
// 構造体①にインターフェースAnimalを実装
func (p *Person) ShowInfo(){
fmt.Println(p.name)
fmt.Println(p.age)
}
// 構造体②にインターフェースAnimalを実装
func (d *Dog) ShowInfo(){
fmt.Println(d.name)
fmt.Println(d.age)
}
func main() {
p := NewPerson("Osamu", 20)
d := NewDog("Pochi", 10)
ShowInfo(p)
ShowInfo(d)
}
空インタフェース
空インタフェースは、全ての型と互換性を持っているため、どんな型の値でも代入することができます。
// 空インターフェース
var a interface{}
// 数値
a = 10
fmt.Printf("%T\n", a) // int
// 文字列
a = "str"
fmt.Printf("%T\n", a) // string
// 配列
a = [...]int{1, 2, 3, 4}
fmt.Printf("%T\n", a) // [...]int
// スライス
a = []string{"a", "b", "c", "d"}
fmt.Printf("%T\n", a) // []string
// マップ
a = map[string]int{"a":1, "b":2, "c":3, "d":4}
fmt.Printf("%T\n", a) // map[string]int
型アサーション
型アサーションとは、インタフェースの値の基になる具体的な値(型)を確かめることでき、インターフェース型から格納されている値の型へ動的に変換することができます。
型アサーションの方法
インタフェースの変数v.(型T)
1つ目の戻り値に、変数vが持っている基になる型Tの値、2つ目の戻り値に、変数vが特定の型を保持しているかどうかの論理値を取得することができます。
変数vが型Tを保持していれば、型Tとtrueを取得でき、保持していなければ、指定した型Tのゼロ値とfalseを取得できます。
インターフェースのゼロ値は、nilなので、初期化せずに型アサーションするとpanicが発生するので注意が必要です。
- インターフェースの基になる型を指定し、アサーション結果は未取得
- インターフェースの基になる型以外を指定し、アサーション結果は未取得
- 型アサーションができず、panic発生
- インターフェースの基になる型を指定し、アサーション結果も取得
- インターフェースの基になる型以外を指定し、アサーション結果も取得
- 型アサーションができなくても、結果を取得することでpanic回避
// ①インターフェースの基になる型を指定
var interface1 interface{} = 10
value1 := interface1.(int)
fmt.Println(value1) // 10
// ②インターフェースの基になる型以外を指定(panic発生)
var interface2 interface{} = 10
value2 := interface2.(string)
fmt.Println(value2)
// ③インターフェースの基になる型を指定し、アサーション結果も取得
var interface3 interface{} = 10
value3, ok3 := interface3.(int)
fmt.Println(value3, ok3) // 10, true
// ④インターフェースの基になる型以外を指定し、アサーション結果も取得
var interface4 interface{} = 10
value4, ok4 := interface4.(string)
fmt.Println(value4, ok4) // 10, false
型switch
データ型の判定は、switch文でも行うことができ、型によって処理を変えることができます。
型switchの方法
switch インターフェースの変数.(type) {case 型1: …, case 型2: …, default: …}
基本的には、switch文と同じように処理を書きます。
func typeTest(i interface{}) {
switch i.(type) {
case int:
fmt.Println(i)
case string:
fmt.Println(i)
default:
fmt.Println("etc")
}
}
func main() {
typeTest(10) // 10
typeTest("abc") // abc
typeTest(true) // etc
}
まとめ
この記事では、前回に引き続きGo言語の基礎文法をまとめました。
少しGo言語をいじってみたいと思ったら、ブラウザ上で実行できる公式チュートリアルでコードを実際に書いてみると雰囲気がつかめるかもしれません。
今後、さらにGo言語のことについて記事にしていきたいと思います。
もし「記事の内容が間違えている!」等ありましたら、Twitterまたはお問い合わせフォームからご連絡いただければと思います。
最後までご覧いただきありがとうございました。