1
0

More than 1 year has passed since last update.

【Go入門】Ubuntuでの環境構築から基礎まで【備忘録】

Posted at

はじめに

この記事はGoについて何も知らなかった時の自分へ向けた備忘録です。

まずはGoの環境構築を行い、最小限のGoプログラムの実行からエラー制御、構造体などについてをまとめています。

環境

  • Ubuntu 22.04.2 LTS
  • go version go1.20.5 linux/amd64

Go環境構築

Goのインストール

以下はUbuntu環境の場合の手順です。その他のプラットフォームをお使いの場合は、Download and install - The Go Programming Languageでインストール手順を確認し、All releases - The Go Programming Languageから必要なファイルをダウンロードしてください。

$ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.5.linux-amd64.tar.gz
.profile
export PATH=$PATH:/usr/local/go/bin
$ source ~/.profile
$ go version
go version go1.20.5 linux/amd64

Vimで快適にGoを書くために

エディタとしてNeovimを、LSPにはcoc.nvimを採用しているため、Vim で Go を書く環境を整えたという記事を参考に環境構築を行いました。

わたしの環境ではcoc-goのインストール時にgoplsもインストールされたため、個別でのインストールは不要でした。

Go基本構文

Goの基本的な構文を確認するために、Hello Worldと出力するプログラムを作成します。

$ mkdir hello
$ cd hello

$ go mod init example.com/hello
$ touch main.go

go mod initコマンドを実行することで、依存関係を追跡するためのgo.modファイルが作成されます。このファイルに依存関係を記述することで、自分のコードがほかのモジュールに含まれるパッケージをインポートすることができます。

以下はHello Worldを出力するmain.goファイルです。

main.go
// エントリポイントとなるパッケージです。
//
// メインパッケージは、mainというパッケージ名を持ち、
// 引数を取らず値を返さないmain関数を宣言する必要があります。
package main

import "fmt"

func main() {
	/**
	* fmt.Println("Heo Word")
	* fmt.println("Hello World") pが小文字
	*/
	fmt.Println("Hello World")
}

上記で作成したプログラムは、go runコマンドで実行することができます。go runは、指定されたmainパッケージをコンパイルして実行するコマンドです。通常はgo run .のようにソースファイルを指定します。

$ go run .
Hello World

パッケージ名とインポート/エクスポート

Effective Go package-namesによると、パッケージ名は小文字の1語であり、アンダースコアや大文字小文字の混在は不要とされています。

fmtパッケージ以外の標準ライブラリを例に挙げると、nettimeのように、小文字の1語となっています。

外部パッケージはimportキーワードでインポートすることができます。上記のサンプルコードでは、I/Oを実装したfmtパッケージをインポートし、fmt.Printlnという関数で、コンソール出力を行っています。

パッケージは1行に1つ記述する方法以外に、()で囲むことでグループ化し、まとめてインポートすることができます。また、エイリアスを付けることも可能です。

package main

import (
	"fmt"
	t "time"
)

func main() {
	fmt.Println("Wait 1sec...")
	t.Sleep(1 * t.Second)
	fmt.Println("Hello World")
}

fmtパッケージのPrintlnや、timeパッケージのSleepのように、外部からインポートした関数はすべて大文字で始まることに注目してください。Goでは名前を大文字ではじめると外部へ公開されるようになります。

例えば、fmtパッケージのprint.goファイルで定義されているnewPrinter関数を呼び出そうとすると、外部へ公開されていない関数のため以下のようなエラーが発生します。

$ go run .
main.go:9:11: undefined: fmt.newPrinter

変数宣言

Goではvarキーワードで変数を宣言することができます。例えばstrというstring型の変数を宣言したい場合は、

var str string

という構文になります。

上記は変数宣言時に初期値を与えない構文であり、このように変数を宣言すると型に応じた「zero value」という値が与えられます。例として、intやfloatなどの数値型には0が、bool型にはfalseが、string型には””(空文字列)が、ポインタ・スライス・マップなどにはnilが与えられます。

変数宣言時に初期値を与えたい場合は:=構文を活用します。例えば上記の変数に対して”string”という値を与えたい場合は、

str := "string"

とすることで初期値を与えることができます。

変数宣言と同時に値を代入する場合は、値から型を推測できるため型の情報は必要ありません。また、varキーワードは不要です。

package main

import "fmt"

var (
	str    string = "string"
	flag          = true
	number int
)

const Tax = 1.08

func main() {
	fmt.Printf("str = %s, flag = %t, number = %d \n", str, flag, number) // str = string, flag = true, number = 0

	numA := 100
	numB := 200
	fmt.Println(numA + numB)
	fmt.Println(float64(numA) * Tax)
}

定数宣言

Goにおいて定数はconstキーワードで宣言することができます。上記のサンプルコードでは、Taxが定数です。

定数は再代入が不可能なため、変数と同じように再代入しようとするとエラーが発生します。

var str = "string"
const Tax = 1.08

func main() {
	str = "striiing"
  // Tax = 1.16 cannot assign to Tax (untyped float constant 1.08)
}

制御構文

If

Goにおいてのもっともシンプルなif文の例です。

count := 0
if count == 0 {
	fmt.Println("countは0です")
}

条件を括弧で囲む必要はありませんが、中括弧は必須です。

またif(とswitch)は初期化文を受け付けるため、ローカル変数の設定に使うことができます。

if _, err := os.Open("invalid.file.txt"); err != nil {
	fmt.Println(err)
}

Goではelse句を使うこともできます。

if文で初期化した変数はその条件分岐と、それに続くすべての分岐で有効です。if文を抜けると使えなくなります。

if num := 10; num < 0 {
	fmt.Println("numは小さい: ", num)
} else {
	fmt.Println("numは大きい: ", num)
}
// fmt.Println(num) undefined: num

2023年7月現在、Goには3項演算子が存在しないため、簡単な条件分岐だろうと完全なif文を記述する必要があります。

For

for文はGoにおいて唯一のループ処理です。

以下は、Goのもっともシンプルなfor文の例です。

i := 1
for i <= 5 {
	fmt.Println(i)
	i = i + 1
}

他の言語でよく見かける一般的な構文を使用することもできます。

for j := 1; j <= 5; j++ {
	if j%2 == 0 {
		continue
	}
	fmt.Println(j)
}

またrange句を活用することで、配列、スライス、文字列、マップなどを簡単にループさせることができます。

nums := []int{1, 2, 3, 4, 5}
sum := 0
for _, num := range nums {
	sum += num
}
fmt.Println(sum)

for _, str := range "string" {
	fmt.Println(str)
}

maps := map[string]string{"name": "test", "password": "Pa$$w0rd"}
for key, value := range maps {
	fmt.Printf("%s:%s \n", key, value)
}

Switch

以下はGoのもっともシンプルなswitch文です。

i := 1
switch i {
case 1:
	fmt.Println("one")
case 2:
	fmt.Println("two")
case 3:
	fmt.Println("three")
}

switch文は、,で区切ることで複数の条件文を追加することができます。また、case句にbool型を返す式を渡すことでif/else文のように扱うこともできます。

switch {
case i <= 0, i > 1:
	fmt.Println("0以下または1より大きい")
default:
	fmt.Println("それ以外")
}

Goのswitch文は、インターフェース変数の動的な型を判定(Type switch)するためにも使用できます。

以下の例では引数tに同じ名前で異なる型の変数を再代入しています。例えばint型だと判定された場合は、その分岐内では変数tはint型になります。

typeSwitch := func(t interface{}) {
	switch t := t.(type) {
	case int:
		fmt.Println("int: ", t)
	case bool:
		fmt.Println("bool: ", t)
	case *int:
		fmt.Println("pointer int: ", *t)
	default:
		fmt.Println("それ以外: ")
	}
}

num := 10
typeSwitch(num) // int
typeSwitch(false) // bool
typeSwitch(&num) // pointer int

エラー制御

Errors

呼び出し元に対して何らかのエラー情報を返したいようなケースはよくあります。Goでは複数の値をリターンすることができる言語仕様を活用し、これを簡単に提供することができます。

例えばosパッケージに含まれるos.Open関数の実装を見てみると、通常の返り値である*File以外にもerror型を返します。

func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}

以下の例では2つ目の返り値がnilかどうかを判定し、nilではない場合はエラーが発生したとしてコンソール出力しています。nil値の場合はエラーなしを表しています。

if _, err := os.Open("invalid.file.txt"); err != nil {
	fmt.Println(err)
}
$ go run .
open invalid.file.txt: no such file or directory

カスタムエラー

またGoではerrorという組み込みインターフェースを実装することで、独自のカスタムエラーを実装することが可能です。

type error interface {
	Error() string
}
package main

import (
	"fmt"
	"unicode/utf8"
)

// カスタムエラー
type invalidValueError struct {
	field   string
	message string
}

func (error *invalidValueError) Error() string {
	return fmt.Sprintf("invalidValueError %v: %v", error.field, error.message)
}

type fileName struct {
	value string
}

// fileName構造体の作成
// バリデーションに失敗した場合は、
// カスタムエラーを返す
func newFileName(value string) (*fileName, error) {
	if len := utf8.RuneCountInString(value); len == 0 {
		return nil, &invalidValueError{
			"fileName",
			"value is required",
		}
	}
	return &fileName{
		value,
	}, nil
}

func main() {
	if _, err := newFileName(""); err != nil {
		fmt.Println(err)
	}
}
$ go run .
invalidValueError fileName: value is required

上記の例では、invalidValueErrorという不正な値を表すカスタムエラーを実装しています。

エラーハンドリング

Goでのエラーハンドリングのbest practicesを調べていく過程で、今goのエラーハンドリングを無難にしておく方法(2021.09現在)という興味深い記事を見つけました。Go2で新しいエラーハンドリングの方法が実装されるまではこちらを参考にしようかなと考えています。

上記の記事によると、fmtパッケージのfmt.Errorf関数でエラーをどんどんラップしていくという方法を取っています。


package main

import (
	"errors"
	"fmt"
)

func usecase() error {
	if err := service(); err != nil {
		return fmt.Errorf("usecase error: %w", err)
	}
	return nil
}
func service() error {
	if err := write(); err != nil {
		return fmt.Errorf("service error: %w", err)
	}
	return nil
}
func write() error {
	return errors.New("failed to write")
}

func getOriginalError(err error) error {
	if wrappedError := errors.Unwrap(err); wrappedError != nil {
		return getOriginalError(wrappedError)
	}
	return err
}

func main() {
	if err := usecase(); err != nil {
		fmt.Println(err)
		fmt.Println(getOriginalError(err))
	}
}
$ go run .
usecase error: service error: failed to write.
failed to write.

確かに開発者側としては、どこでエラーが発生したのか分かりやすい出力になりました。この方法ではユーザー側には不要な情報も含まれるので、errors.Unwrapで大元のエラーを取得するのもありかなと思い、getOriginalErrorという関数も実装してみました。

Panic

通常のエラーであれば追加の返り値としてエラーを返すだけで良いのですが、プログラムが続行できないような回復不可能なエラーが発生することもあります。

このような場面でプログラムを停止させるための組み込み関数panicが用意されています。

panic("paniiiic!")
$ go run .
panic: paniiiic!

goroutine 1 [running]:
main.main()
        /<パス>/main.go:6
exit status 2

Effective Go panicによると、可能な限りパニックは避けるべきとあります。ただしパニックが妥当な例として、セットアップできないという場面を挙げていました。

Githubのプロジェクトを何件か目を通して実際に見かけた例として、プログラムの開始時に環境変数を取得できないようなエラーが発生した場合はパニックを起こしていました。

Recover

recover関数は、パニックを起こしたgoroutineの動作を正常な実行に戻すことができます。recoverは返り値としてpanicに渡された引数を返します。この関数はdefer文の中でのみ有効です。

defer func() {
	fmt.Println("done")
	if err := recover(); err != nil {
		fmt.Printf("run-time panic: %v", err)
	}
}()
fmt.Println("start")

nums := []int{1, 2, 3, 4, 5}
num := nums[10]
fmt.Println(num)
$ go run .
start
done
run-time panic: runtime error: index out of range [10] with length 5

関数

Goの特徴的な点として、関数やメソッドが一度に複数の値を返せることです。これまでにも何度か登場してきましたが、osパッケージのos.Open関数を見てみましょう。

func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}

まずfuncというキーワードがあり、関数名、その後ろには引数が続きます。そしてその後ろでは、返り値がFileのポインタ型とerror型であることを宣言しています。

上記のような複数の値を返す関数から返り値を受け取るには、変数を,で区切ります。不要な値は_とすることで無視することも可能です。

if _, err := os.Open("invalid.file.txt"); err != nil {
	fmt.Println(err)
}

関数とメソッドの違い
Goでは後述のレシーバがある関数のことをメソッド、それ以外の関数をそのまま関数と呼ぶようです。

A method is a function with a receiver.

配列とスライス

配列

Goでも配列をサポートしていますが、特に理由がない限りはスライスが利用されます。逆に、変換行列のような明示的な次元を持つものについては配列が使われます。

Goの配列は要素数も型の一部であり、その要素数は負でないint型の定数である必要があります。

typeSwitch := func(t interface{}) {
	switch t := t.(type) {
	case [1]int:
		fmt.Println("t is [1]int: ", t)
	case [2]int:
		fmt.Println("t is [2]int: ", t)
	default:
		fmt.Println("それ以外")
	}
}

typeSwitch([1]int{1}) // t is [1]int:  [1]
typeSwitch([2]int{1, 2}) // t is [2]int:  [1 2]
typeSwitch([3]int{1, 2, 3}) // それ以外

スライス

スライスは配列をラップしてより強力なインターフェースを開発者に提供してくれます。

以下のコードでは2種類の方法でスライスを作成しています。型Tのスライスは、[]T型として表されます。配列とは異なり、スライスの要素数は型には含めません。

nums := []int{1, 2, 3}
nums2 := make([]int, 3) // [0 0 0]

typeSwitch := func(t interface{}) {
	switch t := t.(type) {
	case [3]int:
		fmt.Println("t is [3]int: ", t)
	case []int:
		fmt.Println("t is []int: ", t)
	default:
		fmt.Println("それ以外")
	}
}
typeSwitch(nums) // t is []int: [1 2 3]

続いて、スライスの要素にアクセスしてみたいと思います。for文のセクションでも紹介しましたが、スライスはrange句で簡単にループさせることができます。また、範囲外の添え字にアクセスするとパニックが起きます。

nums := []int{1, 2, 3}

fmt.Printf("添え字でアクセス nums[0]: %d\n", nums[0])
// fmt.Println(nums[len(nums)]) panic: runtime error: index out of range [3] with length 3

for i, num := range nums {
	fmt.Printf("range句でループ index: %d, num: %d\n", i, num)
}

最後にスライスに対して要素の追加や比較などの操作を行ってみます。

nums1 := []int{1, 2, 3}
nums2 := []int{4, 5}
nums3 := []int{3, 2, 1}

// スライスの長さと容量
fmt.Println(len(nums1)) // 3
fmt.Println(cap(nums1)) // 3

// 要素の追加
fmt.Println(append(nums1, nums2...))    // [1 2 3 4 6]
fmt.Println(append([]int{0}, nums1...)) // [0 1 2 3]

// 要素の削除
index := 1
fmt.Println(append(nums1[:index], nums1[index+1:]...)) // [1 3]

// スライスのソート
sort.Slice(nums3, func(i, j int) bool {
	return nums3[i] < nums3[j]
})
fmt.Println(nums3) // [1 2 3]

// スライスの比較
fmt.Println(reflect.DeepEqual(nums1, nums2)) // false
fmt.Println(reflect.DeepEqual(nums1, nums3)) // trues

マップ

マップはある型の値(キー)と別の型の値(要素)を関連付けるデータ構造です。キーには整数や文字列、構造体など、等式演算子が定義されている型であれば何でも取ることができます。

map1 := map[string]string{"name": "test-user", "password": "Pa$$w0rd"}
map2 := make(map[string]string)

fmt.Println(map1) // map[name:test-user password:Pa$$w0rd]
fmt.Println(map2) // map[]

// 要素の追加・更新
map1["name"] = "test"
fmt.Println(map1["name"]) // test

// 要素の削除
delete(map1, "name")

// 要素の取得
name, ok := map1["name"]
fmt.Printf("map1[name]: %s, isExists: %t\n", name, ok) // "", false

// range句でループ
for key, value := range map1 {
	fmt.Printf("key: %s, value: %s\n", key, value)
}

マップに存在しないキーで要素を取得しようとしてもパニックは起こさず、2つ目の返り値で判定することができます。また要素が存在しない場合には、1つ目の返り値は要素のゼロ値を返します。

構造体

構造体とは

構造体とは型と名前を持ったフィールドの集まりです。フィールド名は構造体内で一意である必要があります。

Errorsセクションでは、カスタムエラーを実装する際に構造体を作成しました。

type invalidValueError struct {
	field   string
	message string
}

上記の例では、fieldmessageというフィールドを持つinvalidValueErrorという構造体を作成しています。

メソッド

Goにはクラスの仕組みはありませんが、型にメソッドを定義することができます。メソッドは関数の構文と似ていますが、funcキーワードとメソッド名の間に「レシーバ」という特別な引数を取ります。

同じくカスタムエラー実装時のコードですが、

func (error *invalidValueError) Error() string {
	return fmt.Sprintf("invalidValueError %v: %v", error.field, error.message)
}

上記がメソッドであり、(error *invalidValueError)の部分を(ポインタ)レシーバと呼びます。レシーバ引数の型がポインタではないレシーバを変数レシーバと呼びますが、こちらはレシーバが指す先の変数を変更できないため、一般的にはポインタレシーバの方が使用されます。

参考資料

1
0
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
1
0