Open6

初めてGoを勉強してみた

R4iw3rR4iw3r

tenntenn氏作成のスライド資料プログラミング言語Go完全入門(https://tenn.in/go)をベースに、Goを1から勉強してみる

基本的には

  • メモしておきたいこと
  • 【TRY】とついている演習課題の解答・詰まったところ

などを残していきたい。
セクション名に埋め込みリンクがなされてるものは【TRY】の該当ページへのリンクである。

R4iw3rR4iw3r

2. 基本構文

おみくじを作ってみよう

いきなり放り出された感じがして少々戸惑ったが、疑似乱数のヒントがあったのでcase文で実装。
以下は自分の回答だが、解答例と違った箇所をdiff表示にしてある。

package main

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

// おみくじを作成するプログラム
// 1~6で擬似ランダムの値を生成し、それにともなっておみくじの結果を出力する
func main() {
  t := time.Now().UnixNano()
  rand.Seed(t)
- s := rand.Intn(7)
+ s := rand.Intn(6) // 0~6の値をこの場合は生成するが、s+1にするのでここは6でいい

- switch s {
+ switch s + 1 { // s + 1にすることで0を引いても問題なく動作させられる
  case 1:
    fmt.Println("凶")
  case 2, 3:
    fmt.Println("吉")
  case 4, 5:
    fmt.Println("中吉")
  case 6:
    fmt.Println("大吉")
- default: //s + 1にすればここは不要
-  fmt.Println("もう一度やり直してください")
  }
}

疑似乱数では0から始まる値が出力されるので、0を引いた際の処理をもう一度引かせるしか思いつかなかったが、caseで最初に与える式をs + 1にすれば良かったみたい。完全にプログラミングの経験不足。
また、VSCodeではrand.Seed(t)に取り消し線が引かれていた。ヒントにあったのをそのまま利用したのだが不要なのだろうか...?

R4iw3rR4iw3r

3.1. 型

組み込み型(数値)

以下のコードが動かない理由を考えて修正するというもの。

package main
func main() {
	var sum int
	sum = 5 + 6 + 3
	avg := sum / 3
	if avg > 4.5 {
		println("good")
	}
}

言わずもがなな気がするが、一応このまま実行してみる。

$ go run builtInTypeNum.go
./builtInTypeNum.go:7:11: 4.5 (untyped float constant) truncated to int

...ですよねー
というわけで、1ページ前に型変換の方法が記述されていたので試してみる。

package main
func main() {
	var sum int
	sum = 5 + 6 + 3
-       avg := sum / 3
+	var avg float64 = float64(sum) / 3
	if avg > 4.5 {
		println("good")
	}
}

変数の省略宣言よく分かっていなかったので律儀にvar avg float64で宣言、float32とどちらにすればいいの迷ったが脳死でfloat64に。

$ go run builtInTypeNum.go
good

いけた。
なお、真偽値バージョンの問題もあったが、真偽値表を埋める問題で、授業で扱われたので大丈夫と信じてスキップ。

コンポジット型

スライスとマップという初見さんの用語がいらっしゃった。

説明
構造体 型の異なるデータ型を集めたデータ型
配列 同じ型のデータを集めて並べたデータ型
スライス 配列の一部を切り出したデータ型
マップ キーと値をマッピングさせたデータ型

(資料p. 15より引用)
スライスはなんとなくこの説明だけでわかったが、マップはいまだによくわからない...

スライスの元となった配列全体の要素数を容量(cap)、スライス自体の要素数を 長さ(len) というらしい。

スライスの利用

以下のコードを3つの変数のみの使用に抑えられるように修正するというもの。

package main
func main() {
	n1 := 19
	n2 := 86
	n3 := 1
	n4 := 12

	sum := n1 + n2 + n3 + n4
	println(sum)
}

3つの変数しか使わないようにするという言葉にピンとこず変な回答になってしまったが、一応以下のように修正を試みた。

package main
func main() {
	num := []int{
	n1 := num[0] 
	n2 := num[2]
	n3 := num[3]
	sum := n1 + n
	println(sum)
}

解答例は以下。for文を使って回すらしい。スライスと合計値を入れる変数、for用の変数で3つに収まっている。

package main

func main() {
	ns := []int{19, 86, 1, 12}
	var sum int
	for i := 0; i < len(ns); i++ {
		sum += ns[i]
	}
	println(sum)
}

マップは以下のように宣言するらしく、json形式みたいだな...?と一旦は理解した。この理解で合ってるのかはわからない。

// 変数名 := map[キーの型]値の型{"キー": 値}
m := map[string]int{"x": 10, "y": 20}

ユーザ定義型

組み込み型などの元からある型を自分のわかりやすい型名をつけて定義できる
ってことで良いのかしら

ユーザ定義型の利用

以下の仕様のデータ構造を定義してみるというもの。

  • とあるゲームの得点を集計をするプログラム
  • ゲームの結果は0点から100点まで1点刻みで点数が付けられる
  • 集計は複数回のゲームの結果をもとにユーザごとに行う
  • どういうデータ構造で1回のゲーム結果を表現すべきか
  • 適切だと思うユーザ定義型を定義してください

題意に添えてるか不安になりながらとりあえず書いた。
左が自分の回答、右が解答例である。

package main

func main() {
	type Score struct {
<		User string 
<		NumberOfGames int
<		GameScore int
>	        UserID string
>	        GameID int
>	        Pont   int
	}
}

組み込み方のインデントが均等になってない、ゲームIDが思いつかず「ゲームの数」とかいうクソダサ型名になってる、とかそういうのは一旦置いといて...
大意は合ってると考えて良いだろう。

R4iw3rR4iw3r

3.2. 関数

go言語の関数は、CやTypeScriptと(おそらく)違って

func add(x int, y int) int {
       return x + y
}

と、戻り値にも型宣言ができるらしい。
また、

func swap(x, y int) (int, int) {
       return x, y
}

型宣言をまとめたり、複数の戻り値の型宣言までできるらしい。すごい。

複数戻り値の利用

以下のmain関数に合うようにswap関数を実装するというもの。

swapFunc.go
package main

func main() {
	n, m := swap(10, 20)
	println(n, m)
}

これでいけた。

swapFunc.go
+ func swap(x, y int) (int, int) {
+	return y, x
+ }

ポインタ

さっきまでは良かったが、次はC言語でも苦手だったポインタ...
さっきと同じで以下のmain関数が動作するようにswap2関数を実装するというもの。

swapPointer.go
package main
func main() {
	n, m := 10, 20
	swap2(&n, &m)
	println(n, m)
}

ポインタ型にするのかしないのかとかで四苦八苦したが、これで通った。

swapPointer.go
+ func swap2(x, y *int) (int, int) {
+	*x, *y = *y, *x
+	return *x, *y
+ }

*と&どっちがどっちなのかいまだに覚えられない...

R4iw3rR4iw3r

3.3. メソッド

メソッド(関数)↔︎レシーバ(変数)って感じでお互いに紐づいてるらしい。
呼び出す時には普通の変数と同じように値がコピーされる。
ポインタを使うことでレシーバの変更を呼び出し元に伝えられる。

正直例をみてもよく理解できなかったので、時間があればGopher道場の動画を見てみようと思う。

レシーバに変更を与える

以下のコードを動くように修正するというもの。ここでのIncメソッドはnを1ずつ加算する。

receiver.go
package main
type MyInt int
func (n MyInt) Inc() { n++ }
func main() {
	var n MyInt
	println(n)
	n.Inc()
	println(n)
}
/*
$ go run receiver.go
0
0

おそらくInc()レシーバを持ってきてもコピーが発生してるから値の増加が反映されない...?
レシーバをそのままいじるにはポインタを使えば良いと説明がなされていたので、やってみる。

receiver.go
package main
type MyInt int
- func (n MyInt) Inc() { n++ }
+ func (n *MyInt) Inc() { *n++ } // ここをポインタに変更
func main() {
	var n MyInt
	println(n)
	(n).Inc()
	println(n)
}
$ go run receiver.go
0
1

できた!

R4iw3rR4iw3r

4. パッケージ

パッケージを分けてみよう

greetingパッケージを自作して、mainパッケージから利用するというもの。

一旦go mod init greetingでプロジェクトを作成。
以下のようなファイルを用意

/main.go
package main

import (
	"fmt"
)

func main() {
	fmt.Println(greeting.Do())
}
/pkg/greeting.go
package greeting

func Do() string {
	return "こんにちは"
}

ここで詰まったのがインポートの仕方。"pkg/greeting"とか"greeting"とかを試してみたのだがうまくいかない...

いろいろ試してたら、以下のようにしたらいけた。

/main.go
package main

import (
	"fmt"

+	"greeting/pkg"
)

func main() {
	fmt.Println(greeting.Do())
}

プロジェクト/モジュールのソースが入っているディレクトリということ?難しい...

modulesを使ってみよう

tenntenn氏が作成されたgreetingモジュールをインポートして使えるようにするというもの。
紛らわしい名前だが、go mod init moduleと、別のプロジェクトを作って試してみる。

main.go
package main

import (
	"fmt"

	greetingv1 "github.com/tenntenn/greeting"
)


func main() {
	fmt.Println(greetingv1.Do())
	// fmt.Println(greetingv2.Do(time.Now()))
}
go.mod
module tryModule

require (
  "github.com/tenntenn/greeting" v1
  // "github.com/tenntenn/greeting" v2
)

go 1.20

問題にはv2.0.0まで使ってみろと合ったが、なぜかできなかった。
どちらか使えればまあ良いだろうと妥協してスキップした。