0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ガチ初心者のGo入門体験

Last updated at Posted at 2025-03-06

はじめに

Go言語というプログラミング言語があります。最近、今後のプロジェクトでバックエンドにGoを使うのはどうか?という雑談がきっかけで入門してみようかと思いましたので、理解の整理も兼ねてここに書きたいと思います。

事前準備

下記のリンクからGoのインストールを行います。

私の場合、miseというパッケージ管理ツールを用いてGoをインストールしました。環境はWSL2で、homebrew経由でインストールしました。

Hello World

Goのプロジェクトを作成するには、以下のようなコマンドを実行します。

  • go mod init <モジュール名>

コマンドを実行すると、「go.mod」というファイルが作成されます。
「go.mod」はこのプロジェクトで利用されるモジュールの依存関係を管理するためのルートになるファイルです。

image.png

「go.mod」と同じ階層に「main.go」を作成し、以下のようなコードを作成します。
Go言語のソースは何らかのパッケージに属す必要があり、mainパッケージは特別なパッケージとして定義されています。このパッケージはmain関数を含み、プログラム実行時のエントリーポイントとなることを示しています。
「fmt」は標準出力などをサポートするフォーマットのパッケージで、デフォルトで提供されているパッケージです。

package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}

このコードを実行します。以下のコマンドを実行します。このコマンドではビルドと実行を同時に行ってくれるので、見かけではインタプリタ言語を実行しているように見えます。

  • go run main.go

「Hello, World!」が出力されています。
image.png

また、Go言語でソースコードをビルドするには、次のコマンドを実行します。
「-o」オプションでビルドした成果物にこちらで名前を付けることができます。(例のコマンドでは「my-app」になります。)

  • go build -o my-app main.go

コマンドを実行すると、「my-app」というバイナリの実行ファイルが作成されます。これを実行するには「./my-app」のようにコマンドを打ちます。例の画像では、「Hello, World!」が出力されていることがわかります。
image.png

モジュール

モジュールとして、外部からインポートを行う例を説明します。「.env」ファイルを扱うモジュール「godotenv」を利用したいとき、画像のようにimportします。するとインポートでエラーになっています。そのモジュールをまだプロジェクトにダウンロードしていないからです。

image.png

モジュールのダウンロードを行うには以下のコマンドを実行します。
このコマンドは必要なモジュールをダウンロードするだけでなく、使っていない不要なモジュールを削除する機能も担っています。

  • go mod tidy

モジュールをダウンロードとすると、その情報「go.mod」に記載されます。「go.sum」には管理されているモジュールのハッシュが記述されており、改ざんの防止や一貫性の担保に寄与しています。
image.png
image.png

外部モジュールのダウンロードができたので、コードを実行できるようになりました。その結果「.env」ファイルから値を取得できました。

package main

import (
	"fmt"
	"os"

	"github.com/joho/godotenv"
)

func main() {
	godotenv.Load()
	fmt.Println(os.Getenv("ENVIRONMENT"))
}

image.png

自作したモジュールを利用することもできます。
例のように、usersフォルダの下に「user.go」を作成し、そこでユーザーIDのようなものを作成する関数を作りました。これを「main.go」で利用したいと思います。
image.png

package users

import (
	"github.com/google/uuid"
)

func GetUser() string {
	uuid, err := uuid.NewRandom()
	if err != nil {
		return "Error generating UUID"
	}
	return "User" + uuid.String()
}

インポートの際に、自作したモジュールがあるフォルダを指定します。
実行すると機能していることがわかります。

package main

import (
	"fmt"
	"my-space/users"
)

func main() {
	user1 := users.GetUser()
	fmt.Println(user1)
}

image.png

モジュールとパッケージのイメージについては以下の図のようになります。モジュールは「go.mod」と「go.sum」を持ちます。モジュールは複数のパッケージを持つことができ、それを利用できます。また、gitなどで公開されているモジュールを外部モジュールとしてダウンロードして用いる事ができます。

image.png

変数

Go言語の変数定義は次のようになります

  • varを用いた宣言
    • var i intのように、varの後に変数名、型を宣言します。 この方法では型宣言は必須ですが、初期値の設定は必須ではありません。初期値を設定しなかった場合は、型に応じた値が設定されます。例えば、int型は「0」、string型は「""」、bool型は「"false"」となります
  • := を用いた宣言
    • i := 1のように宣言をします。この宣言方法では明示的な代入が必須で、型は値に応じて推論されます。また、この宣言方法はトップレベルではエラーになり、関数内で用います。そして宣言した関数の外では利用できないというスコープの違いがあります

変数を出力する際に、「%v」で値を「%T」で型を出力することができます。また「%[1]v」などと書くことで何番めの変数を出力するか指定もできます。

package main

import "fmt"

var greeting1 = "Hello, World!"
var greeting2 = "I am a Go program!"

func main() {
	fmt.Printf("%[1]v %v type:%[1]T \n", greeting1, greeting2)
}

image.png

ポインタ(2025/03/07追記)

プログラミングにおいてポインタは理解が難しい概念の筆頭かと思います。初めてプログラミングに触れたのがC言語で、ポインタでつまずくという経験に共感できる方もおおいと思います。(私がそうでした。)

初めに、ポインタとはメモリ上のアドレスを指し示す変数のことです。
普段変数を使う際は意識しませんが、データを保持するにはその分の容量のメモリが必要で、変数も一定量のメモリを使用します。そのため、変数に値を代入するとき、より細かく言えば変数が確保したメモリ領域に何らかの値が格納されることになります。
メモリ領域は通常byte単位で表現されます。ではある変数が確保しているメモリ領域がどこなのか、それを扱うには変数の前に「&」を付けて実現できます。
実行結果、メモリのアドレスが表示されています。これは変数の1バイト目のメモリアドレスです。

package main

import (
	"fmt"
)

func main() {
	var i int = 1
	fmt.Println(&i)
}

{B8FBF4B4-3621-46E9-A514-24F580BA3E0B}.png

ポインタ型を参照するためには、「*」を付けて表します。例えばvar p *intのように書くとpがint型のポインタ変数になります。
コード例を実行してみると、iのアドレスとpの値は等しいです。これは、代入式からも想像できます。
&pで出力されるpのアドレスはpの値(iのアドレス)異なっていることがわかります。pは独立してアドレス領域を確保し、その値としてiのアドレスを持っていることがわかります。

また変数の前に「*」を付け*pのように書くと、そのポインタ型の変数が値として持っているアドレスに格納されいる値を表示することができます。例の場合だと、pの値はiのアドレスなので、iのアドレスに格納されている値、つまり「1」が出力されます。これを「デリファレンス」といいます。

package main

import (
	"fmt"
)

func main() {
	var i int = 1
	fmt.Println("variable i:")
	fmt.Println(&i)

	var p *int = &i
	fmt.Println("variable p:")
	fmt.Println(p)
	fmt.Println(&p)
	fmt.Println(*p)
}

{62D1D6C2-FE5C-437B-93D6-B577D0AE1BF9}.png

上記の内容をイメージにしたものが下図になります。
{3A04B619-90EF-4461-ADF8-DDE5BB547AEF}.png

スライス

スライスはvar slice []intのように[]<型>という書き方で宣言します。
空のスライス宣言時に、varで宣言した場合、nilとの比較がtrueになるが、:=の宣言ではfalseになります。
つまり、下記のコードだとslice1はtrueになり、slice2はfalseになります。

package main

import "fmt"

func main() {
	var slice1 []int
	slice2 := []int{}

	fmt.Println(slice1 == nil, slice2 == nil)
}

image.png

また要素数が異なる配列は違う型として認識されるので、直接代入することができません。
image.png

要素の追加はappendメソッドを使います。
第一引数で、スライスを指定し、第二引数以降で追加する要素を指定します。
第二引数以降で「スライス名...」と書くことで、異なるスライスを結合可能です。

slice1 = append(slice1, 1, 2, 3)
slice1 = append(slice1, slice2...)

makeによるスライスの初期化

makeメソッドはスライス、マップ、チャネルの初期化を行ってくれる関数です。ここではスライスに関してのみ述べます。スライスの初期化には以下のような構文を用います。
引数はそれぞれスライスの型、要素数の長さ、スライスが確保するメモリ容量です。

  • slice = make([]int, length, capacity)

例として以下のようなスライスを作成し、長さとキャパシティを確認してみます。
結果として要素数は3(すべて0で初期化されています)とキャパシティが5のスライスが作成されていることがわかります。

package main

import "fmt"

func main() {
	var slice1 = make([]int, 3, 5)

	fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))
}

image.png

また、このように作られたスライスに対してappendで要素を追加していくこともできます。そうしてキャパシティを超えてしまった場合でも、自動的にキャパシティを追加で確保してくれるためエラーにはなりません。ただし、キャパシティを追加する際にスライスに対してメモリの再割り当てが起きて動作が遅くなるかもしれませんので、初めから十分なキャパシティを確保したほうがよいかもしれないです。

下のコードのように、初めに要素数3、キャパシティ5のスライスを作ったのち、appendで追加することによってキャパシティも確保されていることがわかります。

package main

import "fmt"

func main() {
	var slice1 = make([]int, 3, 5)

	fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))

	slice1 = append(slice1, 1, 2, 3, 4, 5)

	fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))

	slice2 := []int{10, 20, 30}

	slice1 = append(slice1, slice2...)

	fmt.Printf("slice1: %v len %v cap %v\n", slice1, len(slice1), cap(slice1))
}

image.png

あるスライスを別のスライスにコピーし、コピー先のスライスの要素を変更した場合、コピー元のスライスでも該当する要素の値が変更されます。これは、コピー元、コピー先のスライスが同じメモリを共有しているためです。

package main

import "fmt"

func main() {
	var slice1 = []int{1, 2, 3, 4, 5}
	slice2 := slice1

	fmt.Printf("slice1: %v\n", slice1)

	slice2[0] = 100

	fmt.Printf("slice1: %v\n", slice1)
	fmt.Printf("slice2: %v\n", slice2)
}

image.png

異なるメモリ空間を確保したスライスとしてコピーするには、copyメソッドを用います。

  • copy(<コピー先のスライス名>, <コピー元のスライス名>)

このようにしてスライスをコピーすることで、コピー先スライスの変更はコピー元に影響を及ぼさなくなります。

マップ

マップの作成は以下のようなコードで作成できます。

  • m := map[string]float64{}
  • m := make(map[string]int)

マップのキーを参照した場合、2つの値が返ります。1つがそのキーに対応する値で、もう1つが存在するかしないかのブール値です。そのキーが存在しない場合、値は0になります。
また、deleteメソッドでマップと消したいキーを指定することで、該当のキー、値ペアの削除が可能です。

package main

import (
	"fmt"
)

func main() {
	m1 := make(map[string]map[int]string)

	m1["Alice"] = map[int]string{
		101: "Hello, Alice!",
		102: "Welcome back, Alice!",
	}
	m1["Bob"] = map[int]string{
		201: "Hi, Bob!",
		202: "Good to see you, Bob!",
	}

	m2 := map[string]float64{
		"pi": 3.14,
		"e":  2.71,
	}

	v, ok := m1["Alice"][101]
	if ok {
		fmt.Println(v)
	}

	v2, ok := m2["lambda"]
	if ok {
		fmt.Println(v2)
	} else {
		fmt.Println("Key not found")
	}

	fmt.Println(m1)
	delete(m1, "Alice")
	fmt.Println(m1)
}

image.png

構造体(struct)

構造体type XXX structという形式で表現され、例としては以下のコードのようになります。
user1とuser2を見てわかるように、構造体はスライスと異なりお互いが異なるメモリ領域を確保しているので、user2の変更でコピー元のuser1の値が書き換わることはありません。

また、注意点として構造体を別のパッケージでも扱うためには構造体名、フィールド名(NameやAgeの部分)を大文字から始める必要があります。小文字のものはパッケージ外部から利用できません。日本語のものも利用不可です。

package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	user1 := User{
		Name: "John",
		Age:  30,
	}

	var user2 User = user1
	user2.Name = "Nancy"
	fmt.Printf("%[1]T %+[1]v %v\n", user1, user1.Name)
	fmt.Printf("%[1]T %+[1]v %v\n", user2, user2.Name)
}

image.png

インターフェース

インターフェースも型として実装されるもので、次のような形式で定義されます。これがなにかというと、このインターフェースに定義されたメソッドを持つ型をインターフェースを実装しているとして受け入れる」ということを意味します。

type XXX interface{
   yyy() int
   zzz() float64
   .
   .
   .
}

実際にインターフェースを実装してみた例が以下になります。
「calc」というインターフェースで、2つのメソッドを定義しています。名前から面積と外周を求めるメソッドとわかります。
次に、構造体を2つ定義しています。構造体は円と長方形で、それぞれ半径と横幅・縦幅を持っています。
構造体に対してメソッドを定義しています。メソッドがどの構造体に対応付けられるのか、レシーバーで指定し、メソッドを作成します。
例では、(c Cirle)という部分がレシーバーです。つまり、(c Cirle)という構造体に対して、「float64」型の「CalcArea()」というメソッドを実装します、というコードです。メソッド名と返り値の型がインターフェースで定義したメソッドと一致しています。このように、インターフェースの定義と整合するように、構造体に具体的なメソッドを実装していきます。

コード例では、「Circle」と「Rectangle」という構造体がインターフェースを実装しています。Go言語の特徴で、明示的にインターフェースを実装していなくても、構造体が定義されたインターフェースの内容を満たすようにメソッドを実装している場合は、その構造体は該当のインターフェースを実装しているとみなされます。そのため「Circle」と「Rectangle」はインターフェース「Calc」を実装していると言えます。

.
.
.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
func (c Circle) CalcArea() float64 {
	return 3.14 * c.Radius * c.Radius
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.
.
.

また、Go言語では標準でStringerインターフェースというものを提供しており、型に対してStrig()メソッドを実装すると、その型はStringerインターフェースを実装したものをみなされます。このようにすることで、このインターフェースを実装した型がfmt.Println(xxx)fmt.Printf("%s\n", xxx)を用いて出力するときのフォーマットをこちらで定義できます。

package main

import (
	"fmt"
)

type Calc interface {
	CalcArea() float64
	CalcPerimeter() float64
}

type Circle struct {
	Radius float64
}

func (c Circle) CalcArea() float64 {
	return 3.14 * c.Radius * c.Radius
}

func (c Circle) CalcPerimeter() float64 {
	return 2 * 3.14 * c.Radius
}

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) CalcArea() float64 {
	return r.Width * r.Height
}

func (r Rectangle) CalcPerimeter() float64 {
	return 2 * (r.Width + r.Height)
}

func GetResult(a Calc) {
	fmt.Println("Area: ", a.CalcArea())
	fmt.Println("Perimeter: ", a.CalcPerimeter())
}

func (c Circle) String() string {
	return fmt.Sprintf("Circle with radius: %f, Area: %f, Perimeter: %f", c.Radius, c.CalcArea(), c.CalcPerimeter())
}

func (r Rectangle) String() string {
	return fmt.Sprintf("Rectangle with width: %f, height: %f, Area: %f, Perimeter: %f", r.Width, r.Height, r.CalcArea(), r.CalcPerimeter())
}

func main() {
	circle := Circle{Radius: 5}
	rectangle := Rectangle{Width: 5, Height: 10}

	// Interface
	var calc Calc
	calc = circle
	GetResult(calc)
	calc = rectangle
	GetResult(calc)

	// Interface Stringer
	fmt.Printf("%s\n", circle)
	fmt.Println(rectangle)

}

image.png

実際にコードを実装した結果、上のようになりました。var calc Calcで宣言したインターフェース型の変数に構造体の「circle」と「rectangle」が代入できていることがわかります。構造体がインターフェースを正しく実装できているためです。

関数

関数はfunc XXXX() {}という書き方で作ることができます。コード例の「HelloWorld」関数のように、引数や返り値をとらない関数も作成できます。
CalcAdd関数のように、()内に、引数とその型を指定し、() intのように括弧の後に戻り値の型をしているような関数を作成することができます。

package main

import "fmt"

func HelloWorld() {
	fmt.Println("Hello World! from main.go")
}

func CalcAdd(a int, b int) int {
	return a + b
}

func main() {
	HelloWorld()
	fmt.Println(CalcAdd(1, 2))
}

image.png

無名関数

以下のように名前のない関数を作成することができます。この関数の後ろに()と書いていますが、これによって無名関数に引数として値を与え(引数がない場合はなにも与えず)即座に関数を実行することができます。

package main

import "fmt"

func main() {
	func(a int) {
		a = a * a
		fmt.Println(a)
	}(5)

	func() {
		fmt.Println("Hello")
	}()
}

image.png

無名関数は変数に代入することが可能です。例として以下のようなコードを示します。

package main

import "fmt"

func main() {
	Calcsquare := func(a int) int {
		a = a * a
		return a
	}

	fmt.Println(Calcsquare(5))
	fmt.Println(Calcsquare(15))
}

image.png

また、無名関数は関数の引数になることもできます。「Testmessage」関数の引数に無名関数を「f」として受けとっています。

package main

import "fmt"

func Testmessage(f func(int) int, a int) {
	fmt.Println("Testmessage, id is ", f(a))
}

func main() {
	Calcsquare := func(a int) int {
		a = a * a
		return a
	}

	Testmessage(Calcsquare, 11)
}

image.png

ここでは詳しく述べませんが、関数の返り値として無名関数を指定することも可能です。

制御構文

if

if での条件分岐と、else ifelse が使用できます。else ifelseは直前の} と同じ行に置く必要があります。改行するとエラーになります。

package main

import (
	"fmt"
)

func main() {
	i := 5

	if i == 0 {
		fmt.Println("i is 0")
	} else if i >= 1 {
		fmt.Println("i is greater than 1")
	} else {
		fmt.Println("i is less than 0")
	}

}

image.png

for

forの基本はfor <初期式>; <条件式>; <後処理式>; で表されます。また、スライスやマップに対してrangeを用いる事ができます。rangeで受け取る値は、スライスの場合はインデクス、値であり、マップの場合は、キー、値です。用いない場合は_で受け取ると未使用でもエラーにならず扱えます。

package main

import (
	"fmt"
)

func main() {
	sum := 0
	numbers := []int{10, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30}
	for i := 0; i <= 10; i++ {
		sum += i
	}
	fmt.Println(sum)

	sumNumbers := 0
	for _, v := range numbers {
		sumNumbers += v
	}
	fmt.Println(sumNumbers)
}

image.png

switch

switch <式> {~~~~}で定義されます。case <値>で値が式の結果と一致した際に、その場合のcaseが実行されます。どのcaseとも一致しない場合はdefaultが実行されますが省略可能です。

package main

import (
	"fmt"
)

func main() {
	for i := 0; i <= 3; i++ {
		switch i {
		case 0:
			fmt.Println("Zero")
		case 1:
			fmt.Println("One")
		case 2:
			fmt.Println("Two")
		default:
			fmt.Println("Unknown Number", i)
		}
	}
}

image.png

所感

個人的に、プログラミングは得意でなく、あまりきちんと学習できてきませんでしたが、Goの基本的な書き方は理解しやすかったと感じます。
ただ、並行処理などGoに特徴的な領域についてはまだ触れられてなく、おそらく難しい可と思いますので、丁寧に理解する必要がありそうです。

参考文献

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?