LoginSignup
2
1

More than 5 years have passed since last update.

プログラミング歴半年のRubyistがGoを学び始めて戸惑った点

Last updated at Posted at 2019-04-28

はじめに

プログラミングの実務経験半年ほどのRubyistが、Goを学び始めて戸惑った点をまとめてみました。

戸惑った点その1 例外機構がない

Goには例外機構がありません。Rubyで言うところの begin ~ rescue ~ end みたいな例外処理構文がありません。
例えば何らかの関数を呼び出した際に処理が成功したかどうかを確認するには、戻り値から判断します。

result, err := doSomething()
if (err != nil) {
    // エラー処理
}

errerror型のインタフェースです。Goではこの err を処理することでエラーハンドリングを行います。
(合わせて学ぼう defer, panic and recover

戸惑った点その2 公開範囲は識別子の命名規則による

定数や関数などの公開範囲は、識別子(定数名や関数名)の1文字目が大文字か小文字かによって決まります。1文字目が大文字の場合は他のパッケージから参照可小文字の場合は他のパッケージからは参照不可となります。Rubyで言うprivateみたいな仕組みは、Goでは識別子の命名規則によって実現しています。
例えば以下のようなhogeパッケージの場合、

hoge.go
package hoge

const (
    MAX = 100
    min = 1
)

func DoFoo(n int) int {
    // 何らかの処理
}

func doBar(n int) int {
    // 何らかの処理
}

mainパッケージからの可視範囲は以下のようになります。

main.go
package main

import "hoge"

hoge.MAX    // OK! -> hogeパッケージの定数MAXを参照できる
hoge.min    // NG! -> コンパイルエラー(mainパッケージからhogeパッケージの定数minは参照できない)

hoge.DoFoo(1)    // OK! -> hogeパッケージの関数DoFooを参照できる
hoge.doBar(1)    // NG! -> コンパイルエラー(mainパッケージからhogeパッケージの関数doBarは参照できない)

戸惑った点その3 オブジェクト指向で言うところのコンストラクタがない

Rubyで言うところのinitializeは用意されていません。Goでは型のコンストラクタというパターンを利用します。
型のコンストラクタを表す関数はNew[型名]のように命名し(他のパッケージに公開しないのであれば new[型名])、また、対象の型のポインタ型を返すように定義するのが一般的です。
下記は構造体User型とその初期化のための関数NewUserの例です。

main.go
package main

import "fmt"

type User struct {
    Id   int
    Name string
}

func NewUser(id int, name string) *User {
    u := new(User)
    u.Id = id
    u.Name = name
    return u
}

func main() {
    user := NewUser(1, "Bob")
    fmt.Println(user)  // => &{1, Bob}
}

初期化するという意味で似たニュアンスを持つinit関数がありますが、こちらはパッケージの初期化に使います。
(合わせて学ぼう Package initialization

戸惑った点その4 配列とスライスの定義

Goにおいて「配列」は、拡張や縮小はできずサイズは常に固定です。「スライス」がいわゆる可変長配列にあたります。
これらの型の表現が似ていて少し戸惑いました。

/* 配列を定義 */
// 要素数が3のint型の配列
var a0 [3]int
a1 := [3]int{}
// 要素数は省略可。省略した場合は初期値の数が要素数となる。
a2 := [...]int{1, 2, 3}

/* スライスを定義 */
// int型のスライス
var s0 []int
// 要素数と容量が5であるint型のスライス
s1 := make([]int, 5)  // 初期値指定なし
s2 := []int{1, 2, 3, 4, 5}  // 初期値指定あり

配列は要素数を含めて1つの型なので、型の定義に必ず要素数が必要になります。見た目に関していえば、最初の[]に要素数もしくは...が入ります。
スライスは可変長配列を表現するので、要素数に制限はありません。最初の[]には何も入りません。

戸惑った点その5 ポインタ型の定義

今までポインタを意識する言語はほぼやってこなかったので、ポインタ周りの文法に少し戸惑いました。

ポインタ型は対象の型の前に*をつけることで定義できます。

// int型のポインタ
var p1 *int
// [3]string型のポインタ
var p2 *[3]string

演算子&を使って、任意の型からそのポインタ型を生成することができます。

var i int
p := &i  // pはint型のポインタ

ポインタ型の変数の前*をつけることで、そのポインタ型が指すデータのデリファレンスができます。

var name string
p := &name
name = "Tom"
fmt.Println(*p)  // => "Tom"
*p = "Jerry"
fmt.Println(name)  // => "Jerry"

同じ*でも、型の前についていればポインタ型の定義ポインタ型の変数の前についていればデリファレンスになります。

他に、指定した型のポインタを生成する組み込み関数newがあります。newは主に構造体型のポインタ生成のために利用されるようです。

type User struct {
    Id   int
    Name string
}

p := new(User)  // pは *User型
p.Id  // == 0
p.Name  // == ""

戸惑った点その6 メソッドの意味がオブジェクト指向で言うメソッドとは異なる

Goにおけるメソッドとは、任意の型に特化した関数です。
メソッドの定義は関数の定義と似ていますが、funcとメソッド名の間にレシーバの型とその変数名を書きます。

type Point struct{ X, Y int }

/* *Point型にRenderメソッドを定義 */
func (p *Point) Render() {  // レシーバは*Point型
    fmt.Printf("(%d, %d)\n", p.X, p.Y)
}

メソッドは[レシーバ].[メソッド]の形式で使います。上記のメソッドは以下のように使います。

p := &Point{X:1, Y:2}  // pは *Point型
p.Render()  // => (1, 2)

また、[レシーバの型].[メソッド]でメソッドを関数型として参照することができます。

f := (*Point).Render()
f(&Point{X:1, Y:2})  // => (1, 2)

戸惑った点その7 継承がない

Goはオブジェクト指向言語ではないので、オブジェクト指向で言う「継承」はありません。
Goでは型に柔軟性を持たせるための仕組みとしてインタフェースが用意されています。
インタフェースはinterface{ メソッドのシグネチャの列挙 }の形式で、型が実装すべきメソッドが何かを定義します。

例えばGoの組み込み型error型は以下のようにインタフェースとして定義されています。

type error interface {
    Error() string
}

errorインタフェースを実装した型は以下のように定義します。

type MyError struct {
    Message string
    ErrCode int
}

/* *MyError型にerrorインタフェースのメソッドを実装 */
func (e *MyError) Error() string {
    return e.Message
}

これにより、任意の関数で返すerror型の値としてMyError型を使うことができるようになります。

func RaiseError() error {  // 戻り値の型はerror型
    return MyError{Message: "エラー発生", ErrCode: 1234}  // 戻り値にMyError型を使うことができる

インタフェースのポピュラーな使用方法は「異なる型に共通の性質を付与する」使い方です。
例えば、構造体のUser型とBook型それぞれのポインタ型にStringfyインタフェースを実装することで、それらをStringfy型のデータとしてまとめることができます。

/* Stringfyインタフェースを定義 */
type Stringfy interface {
    ToString() string
}

/* 構造体型Userを定義 */
type User struct {
    Id   int
    Name string
}

/* *User型にToStringメソッドを定義(Stringfyインタフェースを実装) */
func (u *User) ToString() string {
    return fmt.Sprintf("ID: %d, Name: %s", u.Id, u.Name)
}

/* 構造体型Bookを定義 */
type Book struct {
    Id    int
    Title string
}

/* *Book型にToStringメソッドを定義(Stringfyインタフェースを実装) */
func (b *Book) ToString() string {
    return fmt.Sprintf("[ID]%d, [Title]%s", b.Id, b.Title)
}

/* *User型と*Book型をStringfy型のスライスにまとめる */
vs := []Stringfy{
    &User{Id: 1, Name: "Jack"},
    &Book{Id: 101, Title: "スターティングGo言語"},
}

また、インタフェースによって汎用性の高いメソッドを定義することができます。
以下の例では、Stringfyインタフェースを実装した型であれば、関数Printlnを呼び出すことができることを示しています。

func Println(s Stringfy) {
    fmt.Println(s.ToString())
}

Println(&User{Id: 1, Name: "Jack"})  // => "ID: 1, Name: Jack"
Println(&Book{Id: 101, Title: "スターティングGo言語"})  // => "[ID]101, [Title]スターティングGo言語"

(合わせて学ぼう Interface types

おわりに

以上、Goを学び始めて戸惑った点についてまとめてみました。いろいろ戸惑いはしましたが、Go言語、好きです。

参考文献

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1