はじめに
本記事では、Go言語を触ってみたい、基本的な構文を押さえたいと言った初学者に向けた記事となっております。
今回は、DockerでGoの環境構築を行うのでGo自体の環境構築は不要となっております。
エディタはVScodeを使用して進めていきます。
🙏 docker
コマンドが使用できる状態にしておいてください。
Go言語はどのような開発言語なのか?
- Go言語はGoogleによって2009年にリリースされた、比較的新しいプログラミング言語の一つです。
- Goが正式名称ですが、それだけだと別の意味に捉えられてしまうこともあり、「Go言語」や「Golang」の通称で呼ばれています。
Go言語には以下の特徴が挙げられます。
1. コードを簡潔に書くことができる
2. 高速で動作する
3. 並行処理・並列処理に優れている
4. 複雑なシステム構築に向いている
5. 静的型付け言語である
etc....
具体的に一つずつ見ていきましょう。
コードを簡潔に書くことができる
Go言語は、他の言語に比べて機能が削ぎ落とされています。
そのため、コードの記述量が少なく、シンプルで可読性が高い点が特徴といえます。
例えば、繰り返し構文はfor文しかなく、while文やdo/while文などはありません。そのため、for分しか使えないからこそ、コードの統一感が簡単に実現でき、プログラマー毎の表示のばらつきを抑え、コンパイルを高速化することができます。
高速で動作する
記述したコードをプログラムの実行前に一括で機械語に変換する「コンパイラ言語」であるため、一行ずつの変換が必要な「インタプリタ言語」(PythonやPHPなど)と比べ、処理速度が速いです。
並行処理・並列処理に優れている
Go言語は、「並行処理」と「並列処理」が得意な言語です。これも高速処理を実現する要因となっています。並行処理は、複数のコアで一定期間内に複数の処理を順不同に行うことです。対して、並列処理は、複数のCPUを効率良く使うことで、処理速度を向上させ、同時に処理を行うことが可能です。
複雑なシステム構築に向いている
Go言語はコードがシンプルかつ書き方が厳密なので、エンジニアによる癖が現れにくく、複数のエンジニアによる大規模なシステム開発に適しています。
他の人が書いたコードであっても、可読性の高さ故にスムーズに理解することができるため、チームでの開発・運用・保守に向いている言語ともいえます。
一方で、デメリットも存在します。
- 継承ができない
- オブジェクト指向の言語には「継承」というあるクラスの性質を別のクラスに引き継ぐと言った機能が備わっていますが、Go言語は継承がありません
- つまり、既存のコードの使いまわしが難しく、コードの修正もやりにくいと言ったデメリットがあります
- 例外処理がない
- Go言語では、「例外が発生した場合は呼び出し元が処理をする」という考え方に基づいているため、例外処理の機構を用意していません
- この理由は、Go言語がコードのシンプルさを重視しているからです
以上のことから、Go言語のシンプルさは、その機能を削ぎ落とすトレードオフの結果と言えるでしょう。
Goの環境構築をしよう
それではGo言語を実際に触ってみましょう!
まず、作業するディレクトリを作成してその中にmain.go
ファイルを作成してください。
mkdir go-sample && cd go-sample
ローカルにGo言語が入っていなくても動くようにDockerを使います。
作成し終えたら、VScode上でターミナルを開き、以下のコマンドを叩いてください。
docker run -it -v /Users/<User_name>/<directory_name>/<今回作成したdirectory_name>:/app golang
簡単にコマンドの説明をすると、
-
docker run
- Dockerのコンテナを起動させる
-
-it
- コンテナ内でインタラクティブなターミナルを起動し、操作することが可能になる
-
-v /Users/<User_name>/<directory_name>/<今回作成したdirectory_name>
- 今回作業をするディレクトリをコンテナ内にマウントする(
pwd
コマンドで自分が現在いるパスを出力してそれを使う)
- 今回作業をするディレクトリをコンテナ内にマウントする(
-
/app
- コンテナ内のディレクトリの指定
- ホスト側のディレクトリが、コンテナ内の /app ディレクトリとして認識される
-
golang
- 使用するGoのイメージの指定
シェルが立ち上がったらcd /app
でappディレクトリに移動しましょう。
それではGoでHello World!
を出力してみましょう。
main.goファイルに以下の記述をしてください。
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
Goはgo run <ファイル名>
で実行ができます。今回だと、ファイル名にmain.goを指定してあげればOKです👍
動作確認ができたので早速、package
についてです。
package について
- Goのコードは全て何らかのパッケージに属します
- 今回は、main.goのみを扱うので登場するのは、
main package
のみです
- 今回は、main.goのみを扱うので登場するのは、
- パッケージは
import
して使えます- 上記のコードでは、"fmt"(フォーマット)というGoが標準で提供してくれているパッケージを使用しています
- 自分でパッケージを開けながら開発をするめるときもこのようにインポートして使います
- 複数インポートするときは
()
で囲みます -
main.go
package main import ( "fmt" //このパッケージも標準搭載 "math/rand" ) func main() { fmt.Println("Hello World!".rand.Intn(10)) }
- Goはディレクトリがパッケージになります
- ファイル間で要素を移動してもビルド時には差はないです
基本構文について
ここからは基本的な構文を見ていきましょう!
変数について
変数の宣言方法は主に3パターンあります。
1 . 宣言だけしておいて値を後から代入
-
main.go
package main import "fmt" func main() { var i int i = 1 fmt.Println(i) }
-
var 変数名 型
このように宣言できます
-
2 . 宣言と初期化を同時にやる
-
main.go
package main import "fmt" func main() { var i = 1 fmt.Println(i) }
3 .短縮した宣言方法(最もよく使われます)
-
main.go
package main import "fmt" func main() { i := 1 fmt.Println(i) }
-
var
での型宣言は、関数外でも宣言できますが、ショートデクラレーション(:=)は関数内でしか宣言できません - ただ、使える場所ではどんどん短縮形式
:=
を使用するべきです! - 関数外のパッケージ変数の場合は短縮形式が使えないためvarを使用します
複数の変数を同時に初期化することも可能!
main.gopackage main import "fmt" func main() { i, j := 1, "hoge" fmt.Println(i, j) }
ループ処理について
前述した通り、Goでは
for
しかありあません。しかし、while
のような書き方をすることは可能です。 -
-
main.go
package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) }
これは0〜9までの和を計算するコードになります。なので
45
が帰ってきたらOKです!次は、
while
のような書き方を書いてみましょう。main.gopackage main import "fmt" func main() { sum := 1 for sum < 10 { sum += sum } fmt.Println(sum) }
これは変数
sum
が10より小さい範囲で足し算されていきます。続いて、配列をループさせて配列の中身を取り出す処理を書いてみましょう。
main.gopackage main import "fmt" func main() { arr := []int{1, 2, 3, 4, 5} for _, num := range arr { fmt.Println(num) } }
- 配列の宣言方法:
[] 型 {要素1, 要素2, ...}
- 一つ目の変数
_
は配列のインデックスを指します。今回は使用しないでの_
にしています - 二つ目の変数
num
にスライスの各要素が代入されます -
range
はスライスの各要素を1つずつ取り出し、左辺の変数に代入します
- 配列の宣言方法:
条件分岐
配列のループで使ったコードを再利用してif
を使ってみましょう。
-
main.go
package main import "fmt" func main() { arr := []int{1, 2, 3, 4, 5} for _, num := range arr { if num % 2 == 0{ fmt.Printf("%d は偶数です。\n", num) } } }
条件分岐に関しては他の言語とあまり変わらない気がしますね。
関数について
それでは先ほどの条件分岐を関数にしてみましょう。
-
main.go
package main import ( "fmt" ) func isEven(num int, message string) { if num%2 == 0 { fmt.Printf("%d %s \n", num, message) } } func main() { arr := []int{1, 2, 3, 4, 5} for _, num := range arr { isEven(num, "は偶数だ!") } }
func 関数名(引数1, 引数2)戻り値の型{ return 戻り値 }
- 今回は第一引数にint型の
num
、第二引数にstring型のmessageを指定します - 関数の処理の中身は先ほど使用した条件分岐を書きます
- 動的型付け言語と違い、関数定義時に戻り値の型を指定できるので、実行せずともある程度どの型が戻り値かが分かります
構造体について
スクリプト言語しか触ったことがない方は構造体って何?と感じると思うので
まず、構造体とは何かについて話したいと思います。
構造体とは、プログラムを理解しやすくするための小さな構成要素にプログラムを分解する役割を担うものです。
構造体の特徴は以下の3点が挙げられます。
では、実際にコードを見ていきましょう!
まずプロパティだけ持った構造体の宣言をします。
-
main.go
package main import ( "fmt" ) // Animalという構造体を定義 type Animal struct { name string species string age int } func main() { // Animal型のインスタンスを作成 a := Animal{name: "Gopher", species: "mouse", age: 5} fmt.Println(a) // 個々のフィールドを出力 fmt.Println("Name:", a.name) fmt.Println("Species:", a.species) fmt.Println("Age:", a.age) }
- 構造体は
type 構造体名 struct {プロパティ1 型, プロパティ2 型}
このように書きます
- 構造体は
次は、このAnimal構造体にメソッドの追加をしてみましょう!
構造体+メソッド
Goは関数型の言語なのでメソッドの仕方が独特です。
宣言方法は、関数とほとんど同じです。
-
main.go
package main import ( "fmt" ) type Animal struct { name string species string age int } // Animalにメソッドを追加 func (a Animal) Describe() string { return fmt.Sprintf("%s is a %d-year-old %s.", a.name, a.age, a.species) } func main() { a := Animal{name: "Gopher", species: "mouse", age: 5} fmt.Println(a) fmt.Println("Name:", a.name) fmt.Println("Species:", a.species) fmt.Println("Age:", a.age) // Describeメソッドを呼び出して動物の説明を出力 fmt.Println(a.Describe()) }
-
func(レシーバ レシーバの型)メソッド名(引数 型)戻り値の型{return 戻り値}
でメソッドの宣言が可能です -
上記のコードでは
Describe
というメソッドの追加をして、動物の説明を出力します -
メソッドをもう少しクラスっぽく説明すると以下のようになりそうです
-
main.ts
class Animal { readonly name: string; species: string; age: number; constructor(name: string, species: string, age: number) { this.name = name; this.species = species; this.age = age; } describe(): string { return `${this.name} is a ${this.age}-year-old ${this.species}.`; } }
-
Animal
クラスにDescribe
メソッドがありプロパティを持っている感じです
-
ポインターについて
ポインターとは、メモリ上の特定のアドレスを示す際に使用します。
ポインターの重要性は次の点から理解できます。
- プログラムはメモリ上のデータを読み書きすることで機能します。そのため、データがメモリ上のどの位置(アドレス)にあるのか、それがどの範囲にわたっているかを把握する必要があります。
- さらに、変数の型を指定することで、そのポインタが指すメモリ領域のサイズやデータの種類を知ることができます。
また、Go言語のポインターは他の言語(C/ C++)などと比べると、ガベージコレクション3があり、メモリリーク4を防ぐことができます。
ただし、ポインター演算はGoにはなく、安全性やシンプルさゆえ無い機能かなと思います。
メモリの直接操作に関する安全性とシンプルさが確保されているとも言えるでしょう。
それではコードを見ていきましょう!
-
main.go
package main import ( "fmt" ) func main() { i := 10 // iのアドレスを取得 p := &i fmt.Println(*p) }
-
p
- ポインター変数です
-
&変数
- メモリ上のアドレスが取得できます
-
*ポインター変数
-
*
をポインター変数の前につけることで、ポインター変数に入っているアドレスに格納されている値にアクセスできます
-
さらに、*p = 値
をポインター変数p
の後に書くと書き換えができます。
-
main.go
package main import ( "fmt" ) func main() { i := 10 p := &i // 15に書き換える *p = 15 fmt.Println(*p) }
ポインター変数もただの変数なので宣言は変数でした時と変わりません。
-
main.go
package main import "fmt" func main() { var p *int i := 10 p = &i fmt.Println(i)
-
var ポインタ変数 *型
で宣言ができます - ただ、ポインター変数を使うべきでない場面もあります
-
- 小さい構造体や不変のデータには値渡しを使うとき
- ポインタを使うと複雑さが増すとき
- ポインタを適切に管理できず、ガベージコレクションの効率が低下するとき
この3点に気をつけてポインター変数を使いましょう!
さいごに
今回は、Go言語の基本的な概念や構文について軽く触れました。しかし、本記事で紹介した内容以外にも、defer
やclosure
といったGo言語の重要な機能があります。これらはGoを効果的に活用するために欠かせない要素です。
Go言語の基礎を固めるにはA Tour Of Goが個人的にオススメです。興味のある方は是非試してみてください!
参考