BackEnd

Go言語の基礎〜基本構文その2〜

投稿日:2018年10月11日 更新日:

はじめに

昨日に引き続きGo言語の連載3回目です。
基本構文その1より個人的にはGoの特色が出ているだろう基本文法に関して紹介させていただきます。

配列

Goの配列は固定長です。可変長配列は後述するスライスがそれにあたります。たとえば長さが4で要素の型がstringである配列は,次のように宣言します。

配列は、ほかの言語同様に添字でアクセスします。

宣言と同時に初期化することも可能です。その場合は[…]を用いることで,必要な配列の長さを暗黙的に指定できます。

他のプログラミング言語に慣れていると少し奇妙に感じられるかもしれませんが、上記の配列の型名は要素数も含んだ[4]stringとなります。次のarr1とarr2は、要素の型は同じstringですが、長さが違うため配列としては別の型です。関数fnは[4]string型を引数にとるため、型の合わないarr2を渡すとコンパイルエラーになります。

また、配列を渡す場合は値渡しとなり、配列のコピーが渡されます。次のfn()の中で配列に対して行った変更は、main()側には反映されません。

スライス

スライスは、可変長配列として扱うことができます。配列を直接使うのは、シビアなメモリ管理が必要な一部のプログラムだけなので、同じ性質のデータを束ねて扱うという用途であれば、基本的にはスライスを用います。

スライスの宣言

stringのスライスは次のように宣言します。

このように、スライスの型には配列のように長さの情報はありません。
初期化を同時に行う場合は、配列と同じように書くことができます。またスライスも、配列同様に添字でアクセスできます。

len

スライスは可変長配列なので、プログラムの実行時に動的に要素数が変化します。スライスの現在の要素数を調べるには、組み込み関数lenを使用します。

append

スライスの末尾に値を追加する場合はappendを使用します。appendは、スライスの末尾に値を追加し、その結果を返す組込み関数です。複数の値を追加することもできます。

次のように指定すれば、スライスに別のスライスの中身を展開して追加することもできます。

range

配列やスライスに格納された値を、先頭から順番に処理するような場合は、添字によるアクセスの代わりにrangeを使用できます。
for文の中でrangeを用いると、添字と値の両方が取得できます。

実行結果は次のようになります。

rangeは配列やスライスのほかに、string、マップ、チャネルに対しても使用できます。マップについては本記事で、チャネルについてはまた別の記事で解説します。

値の切り出し

string、配列、スライスから、値を部分的に切り出すことができます。次のように始点と終点をコロンで挟んで指定すると、その範囲の値を切り出すことができます。始点、終点を省略した場合、それぞれ先頭、末尾になります。

可変長引数

関数において引数を次のように指定すると、可変長引数として、任意の数の引数をその型のスライスとして受け取ることができます。

nums …intという引数の定義が、可変長引数のすべての値を[]int型のスライスにまとめる指定として機能します。

関数における可変長引数の定義は「引数の末尾に1つだけ定義できる」という制限があります。可変長引数のあとに別の引数を定義したり、複数の可変長引数を定義することはできません。

map

Goの参照型であるmapは、いわゆる「連想配列」に類する、値をKey-Valueの対応で保存するデータ構造です。

宣言と初期化

たとえばintのキーにstringの値を格納するマップは次のように宣言します。

次のようにキーを指定して値を保存します。

宣言と初期化を一緒に行う場合は次のように書きます。

マップの操作

マップから値を取り出す場合は、次のようにキーを指定し、戻り値として受け取ります。

このとき2つ目の戻り値も受け取ると、指定したキーがこのマップに格納されているかをboolで返します。

delete

マップからデータを消す場合は組込み関数のdeleteを使用します。

deleteは「delete(マップ,キーの値)」のように使用します。この際マップに該当するキーの値が存在しなくてもエラーにはならず特に何も処理は行われません。

range

スライス同様、rangeを用いるとfor文でKey-Valueをそれぞれ受け取りながら処理を進めることができます。ただし、マップの場合は取り出される順番は保証されない点に注意してください。

ポインタ

C言語などを学ばれた方であれば、ポインタは既にご存知かと思いますが、Go言語にもポインタというものがあります。ポインタは、データ構造のメモリ上のアドレスと型の情報になります。
ポインタの定義、アドレス演算子(&)やディリファレンス(*)など構文や利用方法は、C言語と同様になります。
但し、ここでは詳しくは触れませんが、一部Go言語ではC言語と異なり、制約があります。ただ、その制約も通常の開発においては気にすることはあまりないと思います。
それでは、サンプルで簡単に説明したいと思いますので、コードとコード内のコメントをみてください。

ポインタは構造体で利用されるケースが多いと思います。スライスやマップなどの参照型でも宣言は可能ではありますが、参照型は型の仕組み自体にポインタを使った参照を含んでいることから利用されるケースはあまりないでしょう。

構造体

Goには、構造体というデータ構造があります。構造体は複数のデータを1つにまとめることが基本的な役割ですが、後述するメソッドを持つことができ、RubyやJavaでのクラスに近い役割も担います。

type

構造体の解説の前に、まずは予約語「type」について確認してみましょう。Goではtypeを用いて既存の型や型リテラルに別名をつけることができます。この際、既存の型とtypeを用いた型は別の型という扱いになります。

typeのあとには、型の名前、その型の定義が続きます。上記では、intを拡張してIDと優先度それぞれに型を定義し、この型を用いて関数の定義を書き換えています。
呼び出す際には、型が適合していないとコンパイルエラーになります。

このように適切な型を用意することで、型レベルの整合性をコンパイル時にチェックでき、堅牢なプログラムを記述できます。IDE(Integrated Development Environment、統合開発環境)のサポートも得やすくなり、リファクタリング時のリグレッションなども防ぎやすくなります。

構造体型の宣言

ここでは、id、detail(タスクの詳細)⁠、done(完了フラグ)の3つのフィールドを持つ、Taskという型を定義してみます。

構造体型もtypeを用いて宣言し、構造体名のあとにそのフィールドを記述します。各フィールドの可視性は名前で決まり、大文字で始まる場合はパブリック、小文字の場合はパッケージ内に閉じたスコープとなります。
この型から値を生成するには、次のように各フィールドに値を割り当てます。

変数taskには、生成された構造体が格納され、各フィールドにはドットでアクセスできます。
構造体に定義した順でパラメータを渡すことで、フィールド名を省略することもできます。

構造体の生成時に値を明示的に指定しなかった場合は、ゼロ値で初期化されます。

メソッド

Goにはメソッドという特徴的な機能があります。メソッドといっても、オブジェクト指向プログラミング言語によくあるメソッドとは異なります。Goのメソッドは、任意の型に特化した関数を定義するための仕組みです。

メソッドの定義

メソッドの定義は以下のように記述します。レシーバーに関する記述をする部分が、関数と異なる箇所になります。

例えばPersonという構造体を定義して、そのPersonのプロフィールを返すメソッドを定義する場合は以下のような記述をします。

型に定義されたメソッドは、 <レシーバ>.<メソッド> という形式で呼び出すことができます。

値渡しとポインタ渡し

メソッドのレシーバーには値渡しかポイント渡しで定義することができます。

値渡しの場合、構造体の新しいコピーが作成され、呼び出されたメソッドに渡されます。コピーの為、メソッド内で値を変更しても呼び出し元は影響を受けません。
ポインタ渡しの場合、構造体のポインタのコピーが渡され、メソッド内で構造体の値を変更した場合、ポインタを経由して書き換えているため、呼び出し側にもその変更が反映されます。次のコードをみてください。

getNameのように単純に構造体の値を参照するメソッドでは、ポインタ渡しにする必要性はないので値渡しで良いと思います。逆にchangeFirstNameのようにメソッド内で構造体の値を変更する場合はポインタ渡しでないと、呼び出し側のmain関数で定義されているpersonの値が変更されません。

値渡しとポインタ渡しのどちらを使用するかですが、値渡しはメソッド内で構造体の状態の変更が行われない為、スレッドセーフに進めることができます。基本は値渡しのレシーバを使用することを推奨しますが、メソッド内でどうしても構造体の値を変更する必要がある、もしくは値渡しでは構造体のコピーを新しく作成する為、構造体が巨大でコストの問題が発生する場合はポインタ渡しにするのが良いと思います。

関数としてのメソッド

メソッドはレシーバーの定義を必要とするなど、通常の関数定義とは少々異なります。しかし、実体としてのメソッドはGoの関数になります。メソッドを関数型として参照するときには「[レシーバーの型].[メソッド]」のように書くことができます。

インターフェース

Go言語におけるオブジェクト指向的な実装をする上で非常に重要な機能であるインターフェースを紹介します。

インターフェースとは

インターフェースは、型の一種であり、メソッドの型だけを宣言したものになります。任意の型に対して、指定のメソッドを実装させることを強制できる仕組みになります。Javaなどの他の言語でもあるインターフェースと同様の扱いです。

ただ、Go言語の場合は、明示的にインターフェースを実装するのではなく、インターフェースが定義しているメソッドを全て実装されていれば、そのインターフェースを実装していることになります。
Go言語は、連載1回目でも軽く触れていますが、クラスという概念がありません。そのため、継承がなく、このインターフェースでのみ、多態性(ポリモーフィズム)を表現できます。
それでは、簡単なサンプルで紹介します。ファイル構成は、下記になります。

まずは、インターフェースを宣言している、greeting.goファイルをみてみましょう。Sayというメソッドを一つだけ宣言しています。また、CreateGreetingというファクトリ関数的なものを用意して、引数に応じてGreetingインターフェースを実装した構造体を返却します。

次にGreetingインターフェースを実装する2つの構造体になります。この2つの構造体は、Sayメソッドを実装しており、返却される文字列が日本語と英語で異なるだけです。

最後に、main.goファイルです。greeting変数は Greeting interface型になりますが、greetingJa構造体もgreetingEn構造体も代入できます。

実行結果は、下記のようになり多態性(ポリモーフィズム)が表現できています。

空インターフェース

インターフェースのおまけとして、 interface{}(空インターフェース)についても軽く触れておきます。この空インターフェースは、実装を強制させるメソッドが何もない(空の)インターフェースとなります。つまり、全ての型と互換性がある特殊な型ということで、どんな型でも代入することができるのです。
下記では、数値と文字列を代入するサンプルを紹介します。

nil

varを使用した変数の定義では、例えばint型の変数の初期値であれば0がセットされますが、interface{}型ではどうなるのでしょうか。

出力させてみると「<nil>」という内容が得られました。nilはGoにおいて「具体的な値を持っていない」状態をあらわす特殊な値です。

型を持つnil

nilは型と値の2つを持ちます。

なので型の違うnil同士だと「==」ではfalseになります。

interface{}での「== nil」

先の例で分かるように、interface{}以外の型であれば型がついていても「== nil」はtrueになります。

arrとarr2はnilの型も値も同じですが、arr2はinterfaceとして扱っており、その場合は型もnilでないと「== nil」はfalseになります。

newとmake

Go言語の基本的なメモリ割り当てには、new()とmake()の2つがあります。これら2つはそれぞれ異なる働きをし、適用先の型も別となります。混乱させてしまうかもしれませんがルールは単純です。

new

まずnew()について説明します。これは組み込み関数であり、他の言語におけるnew()と基本的に同じです。new(T)は、型Tの新しいアイテム用にゼロ化した領域を割り当て、そのアドレスである*T型の値を返します。Go言語風に言い換えると、new(T)が返す値は、新しく割り当てられた型Tのゼロ値のポインタです。

new()から返されるメモリはゼロ化されています。ゼロ化済みオブジェクトは、さらなる初期化を行わなくても使用できるため、こういったオブジェクトの準備にnew()は便利です。すなわち、データ構造体の利用者がnew()でそれを作成すると、すぐに使える状態となります。

make

組み込み関数make(T, args)は、new(T)とは使用目的が異なります。makeで割り当てできるのはスライス、マップ、チャネルだけであり、初期化された、すなわちゼロ値でないT型(*Tでない)の値を返します。makeとnewを使い分ける理由は、これらの3つの型が隠蔽されたデータ構造への参照であり、このデータ構造が使用前に初期化されている必要があるためです。スライスを例にとると、スライスはデータ(配列内)へのポインタ、長さ、キャパシティという3つの情報から構成されており、それらの情報が初期化されるまではスライスの値はnilです。makeはスライス、マップ、チャネルの内部データ構造を初期化し、使用可能となるよう値を準備します。下は、makeの例です。

この例では、100個のintを持つ配列を割り当てたあと、その配列の先頭から10個目までの要素を示す、長さが10でキャパシティ100のスライス構造を作成します。(スライス作成時、キャパシティは省略可能です。動的にキャパシティをオーバーした場合メモリが再度割り当てられます)これに対して、new([]int)は新しくメモリを割り当て、ゼロ化したスライス構造のポインタを返します。つまりこれはnilスライス値へのポインタです。

下は、new()とmake()の違いを例示したものです。

さいごに

今回RE:ENGINESのGo言語勉強会で私が勉強した箇所は以上になります。
Go初心者ではありますが基本構文に関して理解が深まったと思います、まだまだ勉強不足なので勉強は引き続き行い、今回紹介できなかった基礎、基本文法も記事にしていき理解を深めたいと思っています。

Go記事の連載などは、こちらをご覧ください。

おすすめ書籍

スターティングGo言語 (CodeZine BOOKS)  Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る impress top gearシリーズ  みんなのGo言語【現場で使える実践テクニック】  Go言語による並行処理

page_footer_300rect




page_footer_300rect




-BackEnd
-,

執筆者:


comment

メールアドレスが公開されることはありません。

CAPTCHA


関連記事

rails

Ruby2.4でCookieを手動で復号する際に発生したエラーの対処

1 はじめに1.1 前提条件2 発生したエラー2.1 実際のコード2.2 エラー詳細2.3 原因3 どう対処したか3.1 修正後のコード はじめに こんにちは、onoです。 現在開発中のアプリケーショ ...

Go言語

Go言語でテスト作成 testifyの基本的な使い方

1 はじめに2 Goテストフレームワークのスター数3 testifyについて3.1 testifyを導入する方法4 assartionについて4.1 assertion紹介4.2 ElementsMa ...

rails

【Puma】アプリサーバのチューニング

1 はじめに2 チューニングで注意する項目3 子プロセスの数3.1 最低3つの子プロセスを割り当てる3.2 最大子プロセス数3.3 CPUコア数と子プロセス数3.4 なぜPumaの子プロセス数を増やす ...

rails

Shrineでアップロードする際に画像を加工する

1 はじめに2 アップロードする画像のリサイズ2.1 Gemを追加2.2 Uploaderの修正3 サムネイルを作成する3.1 Uploaderの修正3.2 サムネイルを表示する4 バリデーションの追 ...

rails

Railsでの非同期処理とDelayed Job

1 はじめに2 Active Job2.1 Active Jobの役割2.2 ジョブを作成する2.3 ジョブをキューに登録する2.4 コールバック2.5 例外3 Delayed Job3.1 設定3. ...

フォロー

follow us in feedly

AppLink

英語

page_side_300rect

2018年10月
« 9月 11月 »
 123456
78910111213
14151617181920
21222324252627
28293031 

アプリ情報

目標を達成したい方を応援する、TODOアプリもリリースしております。 下記のアイコンから無料でダウンロードできます。

Web版MyCoach

私たちはより広い方にコーチングを知ってもらいたいと考えています。 下記のサイトにて、コーチの方々を紹介しておりますので、よろしければご覧ください。