Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

progateのGoを一通りやってみて、知ったことをざっくりまとめてみた

目次

Goとは

GoはGoogleでRobert Griesemer、Rob Pike、Ken Thompsonによって設計された静的型付けコンパイルプログラミング言語[11]。構文的にはC言語に似ているが、メモリーセーフ、ガベージコレクション、構造型付け、CSPスタイルの並行性[6]がある。以前のドメインgolang.orgからしばしばGolangと呼ばれるが正式名はGoだ。

Goの特徴

  • 文法がシンプル
    • RubyC++を足して2で割ったような感じ。書き味がRubyに似てるが型がある。
    • シンプルさはあるものの、Rubyほど自由度が高く(名前が違うけど同じ機能のメソッドが何個もあったり、構文の書き方が複数存在する)はないので、どの開発者が書いても同じようなコードになる。
  • 実行速度が速い
  • 言語レベルで並行処理をサポートしている
    • Goルーチンとチャネルを使うとできるそう。

Goを書く時の暗黙のルール

  • Goのインデントはスペース4つ
  • Goでは文の末尾にセミコロンをつけない(省略可能)
  • スコープが狭い場合、1文字の変数名が許される

新しく知ったこと

printlnのln

  • printlnのlnは、line(行)の意味です。文字列を出力した後に次の行に自動的に改行されます。printlnを使わないと改行されません。

printlnを使わない場合

package main

func main() {
    print("Hello World")
    print("Hello 世界")
    // => Hello WorldHello 世界
}

printlnを使った場合

package main

func main() {
    println("Hello World")
    println("Hello 世界")  
    /*
        // => Hello World
        // => Hello 世界
    */
}

Goでは println を使うことはほぼなく、fmtパッケージのfmt.Printlnを使います。後者の方が表示できるデータ型が多いです。

ファイル内の構成

Goのコードが書かれているファイルは、パッケージ定義部と関数定義部によって構成されています。

// main.go

// パッケージ定義部分
package main

// 関数定義部分
func main() {
    // 下記の「世界」の部分を「Go」に書き換えてください
    println("Hello, Go")
}

funcキーワード

Goで関数を定義する場合、funcを使います。

func getUser() {
  println("ユーザー")
}

変数宣言

変数はvarで宣言します。関数内部だと :=でvarと型を省略して変数を宣言できます。:=は変数の初期化時に使います。

package main

// .付きインポートをすると、fmt.PrintlnをPrintlnと書くことができる
import . "fmt"

func main() {
    var name string = "yuki"

    // 変数宣言時に初期化をしなくても良い
    // その場合、型を書く
    var name2 string
    name2 = "yukihoge"

    // 値を代入する場合、何を代入するか明らかなので、型を定義しなくても良い
    var otherName = "hoge"

    // goにはvarと型を省略する書き方がある
    userName := "fuga"

    Println(name)       // => yuki
    Println(name2)      // => yukihoge
    Println(otherName)  // => hoge
    Println(userName)   // => fuga
}

変数名はローワーキャメルケース(hogeFuga) or アッパーキャメルケース(HogeFuga)で書きます。 ローワーキャメルケースかアッパーキャメルケースかで、変数の可視性が変わります。前者はプライベート。後者はパブリックになります。パブリックの場合、パッケージを横断して使うことができます。基本的にはローワーキャメルケースで定義したほうが、プライベートになるので良いです。(アッパーキャメルケースだからというわけではなくて、1文字目が大文字だとエクスポートされるという仕組みがGoにあるだけです)

また、Goでは1文字の変数名が許されます。スコープが短い場合、変数名を1文字で書いても可読性が落ちないので、許されているそうです。しかし、その1文字は何でも良いわけではなくて、その変数が何を表しているのか・担っているのか、という本質的な意味を表現する変数名にした方が良いです。lineCount をlではなくcと書くように。

package main

import . "fmt"

func main() {
    c := 1
    Println(c) // => 1
}

定数

constキーワードを使うことで定数を定義できます。定数に := を使用することはできません。自分が見た感じだと定数の命名規則のルールは決まっていないのかと思いました。個人的には変数と同じでローワーキャメルケースとアッパーキャメルケースを使い分けるのがいいのかと思います(Goでは1文字名を大文字で書くとエクスポートされるからです)。

package main

import . "fmt"

func main() {
    const message = "hoge"
    Println(message) // => hoge
}

switch文

Goのswitch文は、caseの最後に暗黙でbreak文が入ります。

package main

func main() {
    n := 3
    switch n {
        case 1:
            println("大吉です")
        case 2, 3:
            println("吉です")
        default:
            println("凶です")
        
    }
}

標準パッケージ

Goには標準パッケージがあるので、自分のプログラムに標準パッケージをインポートすることで、自分で1からプログラムを書かなくても、便利な関数を利用することができます。fmtはよく使います。 Goでは println を使うことはほぼなく、fmtパッケージのfmt.Printlnを使います。後者の方が表示できるデータ型が多いです。

パッケージ名 概要
fmt コンソールに出力できる
math/rand ランダムな数値を生成できる
time 時間に関する処理ができる

ドット付きインポートをすることで、パッケージの関数を呼び出すときに、パッケージ名を省略できます。通常、fmt.Println("hello world") と書くところをドットをつけてインポートすると Println("hello world")と書けます。

// グループ化をすることで、一つのimportステートメントで複数のパッケージをインポートできる
import (
    // ドット操作
    . "fmt"
    "string"
)

fmt.Printf

fmtパッケージのPrintfを使うことで、書式を指定して文字列を出力できます。書式とは出力する文字列の形です。TSのテンプレートリテラルのようなものです。

// 書式の中の%sの部分に、出力に用いる値が入る。
// %sを複数使う場合、出力に用いる値をカンマ区切りで並べていけば良い。
// 文字列を埋め込むなら%s。数値を埋め込むなら%dを使う
// fmt.Printf(書式, 出力に用いる値)

package main

import . "fmt"

func main() {
    a := 25
    n := "yuki"
    Printf("年齢: %d, 名前: %s", a, n) // => 年齢: 25, 名前: yuki
}

Goのfor文

Goのfor文のループで使う変数は、 := を使って宣言しないとエラーが起きます。

package main

import . "fmt"

func main() {
    for i := 0; i < 3; i++ {
        a := 25
        n := "yuki"
        // Printfは改行されないので、改行したい場合、末尾に\n(特殊文字)を書きます
        Printf("年齢: %d, 名前: %s\n", a, n)
        /*
           年齢: 25, 名前: yuki
           年齢: 25, 名前: yuki
           年齢: 25, 名前: yuki
       */
    }
}

乱数

ランダムな数のことを乱数と呼びます。
Goで乱数を扱うためには、math/randというパッケージを使います。 rand.Intn(10)で0 ~ 9の乱数を生成します。rand.Intn(10)では実行するたびに同じ乱数が表示されます。

そのため、完全な乱数を生成するためには、「rand.Seed(time.Now().Unix())」という1行を追加する必要があります。この1行はmain配下に書きます。事前にtimeパッケージをインポートする必要があります。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().Unix())

    for i := 1; i <= 3; i++ {
        fmt.Printf("%d回目のおみくじ結果:", i)
        number := rand.Intn(6)
        switch number {
        case 0:
            fmt.Println("凶です")
        case 1, 2:
            fmt.Println("吉です")
        case 3, 4:
            fmt.Println("中吉です")
        case 5:
            fmt.Println("大吉です")
        }
    }
    /*
       1回目のおみくじ結果:中吉です
       2回目のおみくじ結果:中吉です
       3回目のおみくじ結果:凶です
    */
}

main関数

main関数は、プログラムが実行されると最初に呼び出される特別な関数です。C++とRustと同じです。

fmt.Scan(&変数名)

fmt.Scan(&変数名) を使うと、コンソールでの入力ができます。エンターキーを押すまで、変数の値として認識されます。

package main

import (
    . "fmt"
    "math/rand"
    "time"
)

func main() {
    rand.Seed(time.Now().Unix())

    var input string
    Scan(&input)
    Printf("入力された値: %s", input)
}

関数の命名規則

変数と同じでアッパーキャメル or ローワーキャメルケースにします。 外部に公開する場合、アッパーキャメルケースを使います。

関数の戻り値の型

関数に戻り値がある場合、戻り値の型を定義しないとエラーになります。

package main

import (
    . "fmt"
)

func main() {
    f := "Yuki"
    l := "Haga"
    n := createName(f, l)
    Println(n) // => Haga Yuki
}

// 関数を定義する順番に決まりはない
func createName(first string, last string) string {
    return last + " " + first
}

変数はどこで記録されている

変数はメモリ上のある「場所」に記録されています。この「場所」を表す16進数のことをアドレスと言います。入門者向けの本ではこの16進数を「~番地」で表現することが多いです。

メモリの説明に関しては、以下の記事「第3回:アドレスとポインタ変数 (5/18)」がわかりやすかったので引用させていただきます。

たとえば,C言語プログラム中で int a; と整数の変数を1つ定義すると,整数の値1個を格納する場所がメインメモリ上に確保され,a という名前を使ってこの場所に値を書き込んだり参照したりすることができるようになる。

変数のアドレスを取得するには、「&変数名」とします。 メモリ上の記録する場所はコンピュータによって変わるため、プログラムを実行する度に違うアドレスを出力する場合もあります。

package main

import (
    . "fmt"
)

func main() {
    n := "Yuki"
    Println(n) // => Yuki
    Println(&n) // => 0xc00010a210
}

ポインタ

Goにおけるポインタとは、アドレスのことです。 アドレスは値なので、変数に代入できます。 アドレスが代入された変数のことを、ポインタ型変数と呼びます。 ポインタ型変数を定義するためには、変数のデータ型にアスタリスクをつけます。このデータ型には、ポインタで取得する変数の型を指定します(一般的かわかりませんが、ポインタ型変数の名前の末尾には、Ptrをつけてます)。:=を使ってポインタ型変数を宣言することもできます。

package main

import (
    . "fmt"
)

func main() {
    c := 1
    var cPtr *int = &c
    Println(cPtr) // => 0xc00010a210

    n := "Yuki"
    var nPtr *string = &n
    Println(nPtr) // => 0xc00009e210
}

ポインタ型変数にをつけて「ポインタ型変数名」にすると、ポインタが指し示す変数の「値」を取り出すことができます。

package main

import (
    . "fmt"
)

func main() {
    c, n := 1, "Yuki"
    cPtr, nPtr := &c, &n

    Println(*cPtr) // => 1
    Println(*nPtr) // => Yuki
}

「*ポインタ型変数名 = 更新する値」で、ポインタ型変数を使って、ポインタが指し示す変数の値を更新できます。

package main

import (
    . "fmt"
)

func main() {
    c, n := 1, "Yuki"
    // ポインタ型変数を定義
    cPtr, nPtr := &c, &n

    *cPtr = 10
    Println(*cPtr) // => 10
    Println(c) // => 10
    Println(*nPtr) // => Yuki
}

関数の仮引数をポインタ型にすることができます。関数を呼び出すときの実引数にはアドレスを指定します。

package main

import (
    . "fmt"
)

func main() {
    c := 0

    for i := 0; i < 2; i++ {
        addCount(&c)
        Println(c)
    }
    // addCountの引数の型をポインタ型にしない場合、0 0 が出力される
    /*
       1
       2
   */
}

func addCount(cPtr *int) {
    *cPtr += 1
}

スコープが違うと、同じ変数名でも別の変数として扱われる

スコープが違うと、同じ変数名でも別の変数として扱われます。 本当に別の変数として扱われることを証明するには、ポインタを使います。アドレスが違うので、同じ変数名でも別の変数として扱われていることが分かります。

package main

import (
    . "fmt"
)

func main() {
    c := 0
    Println(&c)
    addCount()
    /*
       0xc0000b2000
       0xc0000b2008
    */
}

func addCount() {
    c := 0
    Println(&c)
}

ポインタの良いところ

ポインタの良いところは、スコープを超えて値を参照・更新できることです。 ポインタ型を関数の引数に指定することで、ポインタ型変数を使って別のスコープの変数を更新できます。

もしポインタを使わない場合、別の関数の結果を受け取りたい場合、変数を定義する必要があります。

package main

import (
    . "fmt"
)

func main() {
    c := 0
    c = addCount(true, c)
    c = addCount(false, c)
    c = addCount(true, c)
    Println(c) // => 2
}

func addCount(isOk bool, c int) int {
    if isOk {
        c += 1
        return c
    } else {
        return c
    }
}

ポインタを使うことで、戻り値を指定したり、変数に再代入させずに、 main関数に定義した変数を別の関数内で更新できます。

package main

import (
    . "fmt"
)

func main() {
    c := 0
    addCount(true, &c)
    addCount(false, &c)
    addCount(true, &c)
    Println(c) // => 2
}

func addCount(isOk bool, c *int) {
    if isOk {
        *c += 1
    }
}

&と*の使い分けを以下の表にまとめます。

ポインタに関する記号 いつ使う
& 変数名の前につけて使う。変数のアドレスを取得したい時に使う。
* ポインタ型変数を宣言したり(型につける)、ポインタ型変数を使ってポインタの指し示す値を参照したり更新する時に使う。関数の引数をポインタ型にしたい時にも使う。

ファイル名

Goを各ファイルはスネークケースで書くのが一般的だそうです。

// 例
main.go
addressed_types_test.go
addressed_types.go

感想

ゴールーチンというものがGoの醍醐味というのをみたので、今度やってみようと思います。

参考記事

Go (プログラミング言語) - Wikipedia

Goルーチンで並行化する方法: 6秒かかる処理を3秒にしよう - Qiita

Short variable declarations

go言語のプロジェクトの雛形を作る | コーラは1日500mlまで

【Go】PackageのPublicとPrivateについて | POST OUTPUT

Go の命名規則 | micnncim

Goだからこそ許される3つの作法 - Qiita

定数を定義する (const, iota) - まくまく Golang ノート

Exported names

他言語プログラマが最低限、気にすべきGoのネーミングルール

第3回 アドレスとポインタ変数

Go言語の変数定義 - Qiita

Goで学ぶポインタとアドレス - Qiita

他言語プログラマが最低限、気にすべきGoのネーミングルール

Pointers

Better Go Playground