Help us understand the problem. What is going on with this article?

今日から始めるGolang【言語の特徴と基本文法】

世間の求人募集を見ての所感ですが、web業界のバックエンドはスクリプト言語はRuby、コンパイラ言語はGoがブームですね。
Ruby, node.jsなどのスクリプト言語ばかりに手をつけていた私も、最近ようやくGoを勉強してみようかなと思い始めました。

今回はGoの特徴と基本文法をまとめました。
これからGo触ってみようかなーといった方の参考になれば幸いです。 :pray:

記載内容に誤りやツッコミどころがありましたら、マサカリ飛ばさずに優しくコメント頂ければ幸いです。 :innocent:

Goの概要

Goは2009年頃にGoogleの内部プロジェクトとして開発がスタートした静的型付け言語です。
開発の発端はGoogleですが、オープンソースプロジェクトです。

Goは当初、Googleのエンジニアが既存の言語(恐らくですが、Googleで特に使われているPython, C++)の良いところ取りをした言語を作るという試みで始まったようです。
構想の主なポイントは下記です(wikiより)。

JavaやC++のように、静的に型付けされ、巨大なシステムでもスケールする
RubyやPythonなどの動的な言語のように生産性が高く、リーダブルであり、過度なボイラープレートが必要ない
IDEが必須ではない。ただし、十分にサポートする
ネットワークおよびマルチプロセッシングをサポートする

超絶ざっくりまとめると、
静的型付けだけど動的型付けみたいに読み書きがしやすい言語
を目指した言語です。

特徴としては、

  • シンプルな言語仕様(継承・Genericsなどがない)
    • HaskellやScalaなどの他の静的型付け言語に比べて学習コストが低い
  • パフォーマンスが良い
    • C++に迫る勢いだとか
  • コンパイルが早い
  • GCとメモリ安全性
  • 平列処理が書きやすい
    • goroutineとchannelの活用
  • Googleの後押しがある
  • Dockerなどの有名なプロジェクトで採用されている実績がある

余談ですが、体感としてはGoよりGolangの方がググラビリティが良いです(特に英語の記事)。

基本文法

ザックリ仕様

  • ファイルの拡張子は.go
  • 変数や関数など、プログラムを構成する要素はすべて何らかのパッケージに属している
  • 一つのファイルに記述できるのは単一のパッケージについてのみである(一つのファイルに複数のパッケージに関する処理は定義できない)
  • ファイルの内部で使用するプログラムを外部から参照する場合にはimportを使用する
  • エントリーポイント(実行が開始される場所)はmainパッケージの中に記載されている処理
  • 一つのディレクトリには一つのパッケージに関する定義のみ記載できる

パッケージのスコープ

Goのスコープの単位は大きい単位順に、パッケージファイル関数ブロック制御構造文がある。
パッケージに定義されている定数、変数、関数などが、他のパッケージから参照可能かどうかは、識別子の一文字目によって決まる。
一文字目が大文字の場合には他のパッケージから参照可能であり、一文字目が小文字の場合には他のパッケージから参照不可能である。

package fruit

const (
  A = "banana", // 外部から参照可能
  b = "apple" // 外部から参照不可能
)

var (
  C = "water melon",  // 外部から参照可能
  d = "pineapple" // 外部から参照不可能
)

// 外部から参照可能
func Harvest() {
 ...
}

// 外部から参照不可能
func cultivate() {
 ...
}

あるパッケージから別のパッケージの変数や処理を参照する場合にはimportを使う。
例えば、mainパッケージから上記のfruitパッケージを参照したい場合には下記のように記載する。

package main

import "fruit" // fruitパッケージをimportする

fruit.A // "banana"を参照
fruit.Harvest() // Harvest()を参照
fruit.cultivate() // fruit内のスコープでのみ使えるので、コンパイルエラーとなる

変数

明示的に宣言する場合にはvarを使う。
型推論で変数を定義する場合には変数名 := 値と記載する。
基本的にはvar宣言しなくても型推論で記載できるので、可読性の観点からも型推論で書く方が良さそう。

// 明示的に宣言する場合
var name string
var x, y int // 複数の変数を一度に定義できる
var ( // ()で記載すると複数の型の変数の定義ができる
 x, y int
 z string
)

// 型推論の場合
x := 1
bol := false

定数

constを用いて定義する。

const hoge = "hoge"
const ( // ()で記載すると複数の定数を一度に記載できる
  fug = "fuga"
  piyo = "piyo"
)

GoにはJavaの列挙型(enum)のような機能は無いが、識別子iotaと定数宣言を用いることで列挙型に近い表現ができる。

const (  // iotaは0から始まり、定義されるたびに1ずつ増える
  a = iota // a == 0
  b = iota // a == 1
  c = iota // a == 2
)

const (  // 2つ目以降の宣言を省略することも可能
  d = iota // a == 0
  e        // a == 1
  f        // a == 2
)

基本型

文字列型

文字列はstring型として定義する。
文字列は""で囲む。

hoge := "ほげ"

//複数行の文字列はバックスラッシュで書くと便利
fuga := `
Hello
World
`

論理値型

論理値はbool型として定義する。
trueまたはfalseの値を持つ。

// varで記載する場合
var boolean bool
boolean = true
// 型推論で記載する場合
bool := false

数値型

数値はint型として定義する。
実装依存を防ぐためにint64やint32と明確に定義することも可能である。

// varで記載する場合
var n int
n = 1
// 型推論で記載する場合
i := 1

数値の型変換をする場合には、明示的に変換後の型で定義する必要がある。

n := 1
b := byte(n) // byte型へ変換
i64 := int64(n) // int64型へ変換
u32 := uint32(n) // uint32型へ変換

浮動小数点

サイズの異なる2つの浮動小数点であるfloat32float64がある。
float32はJavaでいうところのfloatであり、float64はdoubleである。

f64 := 1.0 // float明示的に型を指定しない場合にはfloat64となる
f32 := float32(1.0) // float32を定義する場合には明示的に宣言する必要がある

配列型

配列型を定義する場合には、配列内部に入っている値の型を宣言する必要がある。
明示的に初期値を与えない場合のデフォルトは、文字列は""、整数値は0、真偽値はfalseとなる。

arr := [5]int{1, 2, 3, 4, 5} // [n]intの宣言で、配列に格納する要素数をn個で配列を作る

arr2 := [5]int // == [0, 0, 0, 0, 0] 変数作成時に初期値を指定する必要はない

また、配列内の要素数を明示的に記載しない方法もある。
その場合、初期値で与えられた要素数が、配列の要素数となる。

arr := [...]int{1, 2, 3, 4, 5}

参照型

make

参照型の生成には、組み込み関数のmakeを使う(使わなくても定義できる)。

呼び出し方 型T 意味
make(T, n) スライス 要素数と容量がnであるT型のスライスを生成
make(T, n, m) スライス 要素数がnで容量がmであるT型のスライスを生成
make(T) マップ T型のマップを生成
make(T, n) マップ T型のマップを要素数nをヒントにして生成
make(T) チャネル バッファのないT型のチャネルを生成
make(T, n) チャネル バッファサイズnのT型のチャネルを生成

スライス

いわゆる可変長配列のことである。
スライスは生成時に配列に格納する値の要素数と容量を確保する。

生成時に確保した要素数よりも大きい容量が確保されていると、スライスの要素数を拡張して行く際にメモリ上に新しい領域の確保が不要になる。
その一方で、元の容量よりも大きい数の要素数を入れると、Goは元のスライスが格納していたデータを丸ごと、新しいより大きなメモリ領域へコピーする。

メモリ上の別領域へデータをコピーする処理はコストが高いため、スライスを生成する際にあらかじめ格納される要素数に合わせて容量を確保する方が良い。

var s []int // int型の要素を持つ配列sを生成
s2 := make([]int, 5) // 要素数と容量が5である配列s2を生成

fmt.Println(s2)
// => [0, 0, 0, 0, 0] 初期値を指定していない場合は配列と同様に0が入る

len(s2) // 現在の要素数はlen()を使って調べられる
// => 5

cap(s2) // 現在の容量(capability)はcap()を使って調べられる
// => 5

マップ

いわゆる連想配列のことである。
生成はmap[キーの型]要素の型で定義する。

m := make(map[int]string)
m[1] = "Banana"
m[10] = "Apple"

fmt.Println(m)
// => map[1:Banana 10:Apple]

m2 := map[int]string{1: "Banana", 2: "Apple"} // マップ生成時に初期値を与える場合

チャネル

チャネルはキューの性質を備えるデータ構造である。
チャネルにはキューを格納する領域であるバッファを持っている。
チャネルはキューの性質を使い、複数のゴールーチン間で安全にデータを共有するための仕組みである。

生成は make(chan データ型, バッファサイズ) または chan [データ型]で定義する。
<-chanを使用すると、そのチャネルは受信専用チャネルを意味する。
chan<-を使用すると、そのチャネルは送信専用チャネルを意味する。
指定がない場合には、受信も送信も可能なチャネルとなる。

var ch1 <-chan int // 受信専用チャネル
var ch2 chan<- int // 送信専用チャネル
var ch3 chan int // 送受信どちらでもできるチャネル

ch3 <- 5 // チャネルに整数5を送信
i := <-ch3 // チャネルから整数値を受信

具体的なお話については、下記の方々の記事が参考になると思います。
- GoのChannelを使いこなせるようになるための手引
- Go言語のchannelって一体何よ ~基礎編~【golang】

関数

関数の定義にはfuncを使い、func 関数名(引数 引数の型) 戻り値の型 {}で宣言する。
戻り値を持つ場合には、戻り値を返す場所でreturnをつける。

func plus(x, y int) int { // 引数x,yはint型であり、戻り値もint型
  return x + y
}

複数の戻り値を返すこともできる。

func div(a, b int) (int, int) { // 引数a,bはint型、戻り値はint型の値が2つ返される
  q := a / b
  r := a % b
  return q, r
}

func main() {
  q, r := div(19, 7)
  fmt.Println("商=%d 剰余=%d\n", q, r)
}

戻り値を破棄する場合には_を使う。
上記の関数divの例を使うとこん感じで書ける。

q, _ := div(19, 7) // 戻り値の2つ目は破棄される

エラーハンドリングは下記のようにするのが一般的っぽい。

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

制御文

if

他言語とほぼ同じであるが、{}は省略不可である。

if x == 1 {
  // x==1の場合に実行する処理
} else if x == 2 {
  // x==2の場合に実行する処理
} else {
  // xが1でも2でもない場合に実行する処理
}

for

条件なしのforは無限ループとなる。

for {
  fmt.Println("I am in infinite loop")
}

典型的なforはこんな感じ

for i := 0; i < 100; i++ {
  fmt.Prinltn(i)
  i++
}

rangeと絡めて使うことも多いとか

fruit := [3]string{"Apple", "Banana", "Melon"}
for i, s := range fruit { // i=配列のインデックス、s=配列内の要素
  fmt.Printf("fruit[%d]=%s\n", i, s)
}

// => fruit[0]=Apple
// => fruit[1]=Banana
// => fruit[2]=Melon

switch

n := 3
switch n {
case 1, 2:
  fmt.Println("1 or 2")
case 3:
  fmt.Println("3")
default:
  fmt.Println("others")
}

// => 3

defer

関数の終了時に実行される式を登録できる。
一つの関数内での登録は幾つでもできるが、実行される順番はあとで登録されたものから順番に実行される。

func main() {
  defer fmt.Println("Hello GO!")
  defer fmt.Println("Hello World!")
  fmt.Println("done")
}

// => done
// => Hello World!
// => Hello GO!

panic/recover

Goのランタイムを強制的に停止させる機能を持つ。
panicはプログラムにおいて、これ以上処理を継続できない状態を意味するので、他言語で言う所の例外処理と同じように多用すべきではない。

panicを実行するとランタイムパニック(run-time panic)が発生し、実行中の関数は中断される。
ただし、中断時までに登録されたdeferは全て実行される

package main
import "fmt"

func main() {
  defer fmt.Println("Hello go!") // panic発生時も実行される
  panic("runtime error!!!")  // ここで処理が終了する
  fmt.Println("Hello world") // これは実行されない
}

panicで上がってきたrun-time errorによるプログラム中断を回復するのがrecoverである。
panicが発生した際にrecoverを実行するためには、recoverをdeferと一緒に使う。
recoverはinterface{}型を戻り値とし、その値がnilではない場合にpanicが実行されたと判断する。

func main() {
  defer func() {
    if x := recover(); x != nil { // panicが発生した場合、 x != nilはtrueとなる
      fmt.Println(x) // 変数xは、panicに渡されたinterface{}
    }
  }()
  panic("Panic occured!!")
  fmt.Println("Hello Go!") // これは実行されない
}

// => Panic occured!!

go

並行処理を司る機能である。
ゴールーチン(goroutine)と呼ばれる、スレッドよりも小さい処理単位で並行して動作する。
deferと同様に、関数呼び出し形式の式を受け取る。

package main

import "fmt"
func sub() {
    for {
        fmt.Println("sub loop")
    }
}

func main() {
    go sub() //ゴールーチン開始
    for {
        fmt.Println("main loop")
    }
}

/*
sub loop
sub loop
sub loop
main loop
main loop
sub loop
...
*/

構造体とインターフェース

Goにはクラスという概念はなく、代わりに構造体(struct)とインターフェース(interface)を用いる。

ポインタ

GoにはC言語でも使われているポインタの概念がある。
ポインタとは、あるデータ構造のメモリ上のアドレスと型の情報である。

ポインタ型は*intのように、ポインタ(*)を操作・参照する対象の型の前に置くことで定義する。

演算子&(アドレス演算子)を用いて、任意の型からそのポインタ型を生成することができる。

var i int
p := &i // iのポインタ型を生成
fmt.Printf("%T\n", p) // => "*int"

ポインタからの変数から値を参照するには、演算子*をポインタ型の変数の前に置くことで、ポインタ型が指し示すデータのデリファレンス(ポインタ型が保持するメモリ上のアドレスを経由して、データ本体を参照する)することが可能である。

var i int
p := &i // iのポインタ型
i = 5
fmt.Println(*p)
// => 5
*p = 10
fmt.Println(i) // iと同じメモリ上の値を書き換えたのでiの参照するあたいも5 -> 10に変わる
// => 10

ポインタについては、こちらの方々の記事がわかりやすいと思います。
- 【Go言語入門】構造体とポインタについて
- Goで学ぶポインタとアドレス
- Goのポインタ

構造体

オブジェクト指向言語で言う所のクラスに相当し、複数の任意の型の値を一つにまとめたものである。
構造体の定義はtype 構造体の名前 struct {フィールド}として宣言できる。

type Point struct {
  x int
  y int
}

type Point2 struct {
  x, y int // 同じ型のフィールドは一括で宣言できる
}

構造体とポインタ

構造体は値型であるため、関数の引数に構造体を渡した場合には構造体のコピーが生成される。そのため元の構造体に対して影響を与えることができない。

type Point struct {
  X, Y int
}

func swap(p Point) { // Pointのstruct型を持つpを引数に持つ
  x, y := p.Y, p.X
  p.X = x
  p.Y = y
}

func main() {
  p := Point{X:1, Y: 2}
  swap(p) // 値渡しで処理される
  fmt.Println(p.X) // 1
  fmt.Println(p.Y) // 2
}

元の構造体に対して影響を与えるためには(参照渡しで処理を実行するためには)、呼び出す関数側の引数を構造体型へのポインタを受け取るようにする必要がある。

type Point struct {
  X, Y int
}

func swap(p *Point) { // Point型のポインタを受け取るようにする
  x, y := p.Y, p.X
  p.X = x
  p.Y = y
}

func main() {
  p := Point{X:1, Y: 2}
  swap(&p) // Point型のポインタを渡す
  fmt.Println(p.X) // 2
  fmt.Println(p.Y) // 1
}

指定した型のポインタ型を生成するには関数newがある。

type Person struct {
  Name string
  Age int
}

p := new(Person) // pは*Person型

メソッド

Goのメソッドは、任意の型に特化した関数を定義することで実現できる。
メソッドの定義の際、funcの直後にレシーバーの型とその変数名が必要になる。
定義したメソッドはレシーバー.メソッドで呼び出すことができる。

type Person struct {
  Name string
  Age int
}

func (p *Person) SayHello() { // *Person型のメソッドSayHello
  fmt.Println("Hello World!")
}

func main() {
  p := &Person{Name: "山田太郎", Age: 30}
  p.SayHello()
  // => Hello World!
}

インターフェース

インターフェースは型の一つであり、任意の型がどのようなメソッドを実装すべきかを定義するものである。
インターフェースの宣言はtype インターフェースの名前 interface{}と記述する。

インターフェースを用いることで、異なる型に対して共通の性質を付与することができる。
これにより、汎用性の高い関数やメソッドを定義することができる。

type Stringify interface {
  ToString() string
}

// Person型
type Person struct {
  Name string
  Age int
}

func (p *Person) ToString() string {
  return fmt.Println("%s(%d)", p.Name, p.Age)
}

// Car型
type Car struct {
  Number string
  Model string
}

func (c *Car) ToString() string {
  return fmt.Println("%s(%d)", c.Number, c.Model)
}

// 異なる型を共通のインターフェース型にまとめる
vs := []Stringify {
  &Person{Name: "山田太郎", Age: 30},
  &Car{Number: "ぬ-100-001", Model: "ZR103"},
}

foo _, v := range vs {
  fmt.Println(v.ToString())
}

// => 山田太郎(30)
// => ぬ-100-001(ZR103)

学習に役立ちそうなサイト/書籍

まとめ(所感)

Goは静的型付け言語を扱った経験が乏しいエンジニア(私)にも優しい文法になっていて、学習コストが低そうだなという印象です。
速くて書きやすいという点で、さすがGoogleのエンジニアが生みの親なだけはあるなという感じ。
その一方で、Go特有の概念(ポインタ、panicなど)はちゃんと抑えないとね。

合わせて読みたい

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away