Udemyにて、Go言語の講座を受講した際の気づきをメモする(セクション8まで)
基本情報
受講している講座は以下
現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発
始めた時の状態
Go言語の入門用書籍(初めてのGo言語)を7割程度実施、その後、AtCoderでABCの問題A、B、C程度の問題を20問程度解いた状態での受講。
簡単なコードをたくさん書くことで、極基礎の部分の文法はすらすら出てくるレベルにはなっている。
この講座のセクション8までの感想
前半の部分のGoの基本文法のセクションでは、かなり網羅的にGoの文法を扱ってくれている。
反面、なぜそうするのか、いつ使うべきか、などの説明はない。
しかし、ビデオ講座でここまで網羅的に文法を扱ってくれるのはすごいな、とは思いました。
私自身は、この講座の前に、「初めてのGo言語」である程度勉強しています。
「初めてのGo言語」では、「なぜ」や「いつ」についてもかなり詳しく説明がなされいます。
反面、そういったケースの詳しい解説が入る関係で、(初心者の段階では)情報過多で消化不良を起こし勝ちです。
ということで、逆順でやった方がよかったのかな、と思っています。
8.変数宣言
var (
i int = 1
f64 float64 = 1.2
)
var
は複数宣言したい場合には括弧でくくることができる。
※書籍に書いてあった気がするが、余り使う機会がなかった。パッケージを作成する場合には使うのだろうか。
フォーマットの動詞%T
で変数の型を表示させることができる。
※デバッグ用?
9.const
- リテラルに名前を付与するだけの機能
- 型を指定してconstを宣言することもできる
という2点が説明されていなかった
11.文字列
バックコートで囲うと、入力したものをそのまま文字列とすることができる。
例えば、ダブルコートを文字列に含めたい場合、ダブルコートだけをバックスラッシュでエスケープする方法と、文字列全体をバックコートで囲む方法がある。
13.型変換
strconv.Atoi
が出てくるけど、カンマokイディオムとか、リターンで複数の値を返せるとか、そういう部分の説明がない。
14.配列
配列の宣言方法
var a [2]int
配列の宣言方法覚えてなかった…。
が、利用するAPIの戻り値が配列だった、とかでなければ使うこともないような…。
15.スライス
スライスの部分スライスをとって、それを変更した場合どうなるか、という議論がなかった。
初心者向けなので問題はないと思うが、そういう使い方しない方がよい(初心者のうちは)、みたいな情報があってもよかったかもしれない。
16.スライスのmakeとcap
スライスがnilの場合、appendが使えないのかと思っていたが、普通にappendは使えた。
17.map
存在しないキーでも聞けてしまって、ゼロ値が返ってくる、という説明が欠落しているような。※他の言語とは異なる部分なので。
これも、自分の知識不足だが、makeは引数として型だけ指定して、数値を指定しないくてもアドレスの確保がされるらしい。
18.関数
これは完全に「初めてのGo言語」の受け売りだが、名前付き戻り値は、
- シャドーイングの可能性
- 名前付き戻り値を定義しても利用が強制されない
という問題?を孕んでいるため、余り使わない方がよいのでは、と思ってしまう。
20.クロージャ
クロージャの使い方、わかりやすかった。
func incrementGenerator() func() int {
x := 0
return func() int {
x++
return x
}
}
func main() {
counter := incrementGenerator()
fmt.Println(counter())
fmt.Println(counter())
fmt.Println(counter())
}
27.switch
ブランクstitchを使う場合には、各caseをキチンと考えましょう。通常のswitchで書けるかも。
※初めてのGo言語の受け売り
29.log
Goには他言語のようなlogの仕組みはなく、他言語のようなロギングをしたい場合には、サードパーティ製品を使った方が良いかもしれない。
log.Fatalxxxxを使うと、ログを書いた後にプログラムが終了してしまう。
logファイルへの書き込みについて
ここから突如難しくなるので注意。
ログファイルの指定やフラグの設定がない(デフォルトの)場合には、標準出力に指定されたメッセージをする。
設定がある場合には、設定に従う。
io.MultiWriter
は、複数の対象に書き込むWriter。この場合には、標準出力とログ用に開いたファイル。
func LoggingSettings(logFile string) {
logfile, _ := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
multiLogFile := io.MultiWriter(os.Stdout, logfile)
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
log.SetOutput(multiLogFile)
}
func main() {
LoggingSettings("test.log")
_, err := os.Open("fdafdsafa")
if err != nil {
log.Fatalln("Exit", err)
}
}
31.panicとrecover
Go言語では、エラーハンドリングをきちっとやることが推奨されている。
自分で書くコードでは、panicは極力利用しないようにすること。
例えば、割り算における0割のような事前のチェックで判別できるようなものは、panicではなくエラーであるということ。自分の力ではどうにもならない状況が発生したときにのみpanicを利用する。
つまり、そのpanicを回復するrecoverというのは、かなり稀な状況で利用される。例えば、ネットワークの瞬断が起こるような場合で、リトライで回復が望めるような場合とか?
34.ポインタ
var v int = 100
var p *int = &v
- ポインタの型宣言には、
*
が付く - 値のアドレスを表現するのは、
&
が付く - アドレスが差す値を表現するには、
*
を付ける
35.newとmakeの違い
宣言をして、型を確認した時に、
- ポインタ型となるもの:newを使う
- そうでないもの:makeを使う
「初めてのGo言語」では、スライスとマップはそもそもがポインタである、という説明がなされていた。説明の切り口が異なる。
36.struct
初めてのGo言語によると、構造体のポイントを作りたいときは、newを使わずに&を付ける方が多いとのこと。
構造体を関数の引数にポインタ渡しする場合の挙動は特殊。
(*s).a
と書かなくてもs.a
でアクセス可能。
ただ、データはイミュータブルとして扱った方が良い、という観点から
構造体をポインタ渡しして、値を書き換えるのは極力控えた方が良い(初めてのGo言語)。
40.コンストラクタ
パッケージにNew関数を定義してあげて、構造体の値(インスタンス?)を作成する。
でもなんで、ポインタを返すのだろう…。
41.Embedded
まるで継承かのような説明がなされているが、コンポジションにあたるものだと思う。
継承のように、埋め込まれた型は、埋め込んだ型のインスタンスとして扱うことはできない、とかその辺の説明がなかった。
42.non-structのメソッド
dotnet系とかの言語の拡張メソッドの代わりになるものかな?
ただし、拡張したことが明示的にわかるように別の型として宣言する必要がある、ということか。
43 インターフェースとダックタイピング
40.コンストラクタのところで気になっていたポインタだが、
インタフェースがあり、そのインターフェースの実装(メソッド)がポインタレシーバであれば、
インターフェースの値には、ポインタを渡す必要がある。※難しい…。
で、こういうことがあるので、ポインタを常に返しておいた方が無難、ということなのだろうか。
※いつ、どのインターフェースが適用されるかは、実装サイドでは判断できないので。
タイトルに「ダックタイピング」とつけているが、ダックタイピングに関する説明は全くない。Goのインターフェース周りの仕様が、ダックタイピングを実現するためのもの、ということなのだが、プログラム初心者にはダックタイピングそのものが分からないと考えられる。
44.タイプアサーションとswitch type文
よく使われるパターンという説明があったが、どういうときによく使うのだろう。
フレームワーク系の処理でも書かないとほとんど使う機会がなさそうなのだけど。
関数型プログラミングのパターンマッチのような形で使う実装パターンが存在するのだろうか。
func do(i interface{}) {
/*
ii := i.(int)
// i = ii * 2
ii *= 2
fmt.Println(ii)
ss := i.(string)
fmt.Println(ss + "!")
*/
switch v := i.(type) {
case int:
fmt.Println(v * 2)
case string:
fmt.Println(v + "!")
default:
fmt.Printf("I don't know %T\n", v)
}
}
45.Stringer
toStringメソッドを定義するようなイメージだが、明示的に呼び出す必要がないというところが異なる。
ログやデバッグの際などに便利そう。
func (p Person) String() string {
return fmt.Sprintf("My name is %v.", p.Name)
}
func main() {
mike := Person{"Mike", 22}
fmt.Println(mike)
}
46.カスタムエラー
エラーを定義する際はポインタレシーバにして、ポインタ渡しすべき(イディオムとして覚える)
と解説されていたが、「初めてのGo言語」にはそういった記述はない。悩む。
func (e *UserNotFound) Error() string {
return fmt.Sprintf("User not found: %v", e.Username)
}
func myFunc() error {
// Something wrong
ok := false
if ok {
return nil
}
return &UserNotFound{Username: "mike"}
}
49.gorutineとsync.WaitGroup
何もしないとgoルーチンにしたものの実行の終了をプログラムは待ってくれない。
sync.WaitGroupを使うことで、終了を待つことができるようになる。
func goroutine(s string, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
//time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go goroutine("world", &wg)
normal("hello")
// time.Sleep(2000 * time.Millisecond)
wg.Wait()
}
50.channel
チャネルを使っている場合は、sync.WaitGroupを使わなくても待ってくれる。
func goroutine1(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum
}
func main() {
s := []int{1, 2, 3, 4, 5}
c := make(chan int) // 15, 15
go goroutine1(s, c)
go goroutine1(s, c)
x := <-c
fmt.Println(x)
y := <-c
fmt.Println(y)
}
同じチャネルに複数のGoルーチンを紐づけした場合、結果を区別することはできないっぽい。終わった順に処理される。
51.Buffered Channels
以下で、2個までに限定したチャネルを作成できる。
ch := make(chan int, 2)
チャネルから値を取り出したら、チャネル内の取り出した値は削除される(当たり前と言えば当たり前)。
ということで、for文で回したい場合には、チャネルをクローズする。
rangeで回す場合、チャンネルに中身がなくても、次の値を取りに行ってしまうため。
close(ch)
52.channelのrangeとclose
この書き方で、実行が終わった都度結果が表示されるようになる。
func goroutine1(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
c <- sum
}
close(c)
}
func main() {
s := []int{1, 2, 3, 4, 5}
c := make(chan int, len(s))
go goroutine1(s, c)
for i := range c {
fmt.Println(i)
}
}
53.producerとconsumer
提示されたサンプルコードだと、余り2つのGoルーチンを使っている感覚が得られないので、producerの方に、乱数でスリープする時間を調整するコードを追加してみた。
自分の理解不足により、wg.Wait()
以下の処理がよくわからなかったが、
wg.Wait()
は、「wg.Add
した数字が0になるまで待つ」ので、処理が全部終わったらclose(ch)
以下の処理が実行される。
func producer(ch chan int, i int) {
time.Sleep(time.Duration(100*rand.Intn(20)) * time.Millisecond)
ch <- i * 2
}
func consumer(ch chan int, wg *sync.WaitGroup) {
for i := range ch {
func() {
defer wg.Done()
fmt.Println("process", i*1000)
}()
}
}
func main() {
var wg sync.WaitGroup
ch := make(chan int)
// Producer
for i := 0; i < 10; i++ {
wg.Add(1)
go producer(ch, i)
}
// Consumer
go consumer(ch, &wg)
wg.Wait()
close(ch)
fmt.Println("Done")
}
54.fan-out fan-in
チャネルの方向をシグネチャに書けるのわかりやすい。
func multi2(first <-chan int, second chan<- int) {
パイプラインの形にしておいた方が、後々のメンテナンス性が上がる。
55. channelとselect
これで、どちらが来ても即座に処理ができるということ。
for {
select {
case msg1 := <-c1:
fmt.Println(msg1)
case msg2 := <-c2:
fmt.Println(msg2)
}
}
こう書いてしまうと、c1が来るまで待ち、次にc2が来るまで待ち、となってしまう。
for {
msg1 := <-c1:
fmt.Println(msg1)
msg2 := <-c2:
fmt.Println(msg2)
}
56.Default Selectionとfor break
何を説明しているのかわかりにくい気がする。
まず、select
にdefault
ケースを設定できる。通常、select
のケースはチャネルからの情報の取得なので、チャネルが何も返してこなければ、「待ち」になるはずだが、default
を置くことで、どのチャネルも値を返してこないときの処理を書くことができる。
次に、break
の特殊な使い方。select
にもbreak
を使う構文があるので、select
の中にbreak
を書いてもselect
しか抜けられない。このため、何を抜けるのかを明確に指定できるような構文がある、ということ。
57.stync.Mutex
「初めてのGo言語」ではGoルーチンのところは未学習だったので、Mutexについての章を確認してみたところ。
使わなくて済むなら、使わない方がよい。ただし、使うことによって処理が簡単になる場合もある。
という感じでした。
61.PublicとPrivate
パッケージの中で、先頭大文字で宣言するとPublic、小文字で宣言するとPrivate
特殊。なれるのに時間がかかりそう。
65.godoc
例は、テストの方に書くというには、理にかなっている気がするし、実際のコードを書くというのも分かりやすい。さすが新しい言語という気がする。