[輪読]Effective Goを読んでいく
イントロダクション
このドキュメントは明確で慣用的なGoのコードを書くためのヒントを示します。言語仕様 や Tour of Go や How to Write Go Code を補強するもので、最初に読むべきものです。
Goをやっていくための一歩目として間違ってなさそうだったので迷わず読んでいけそう
多くのパッケージでは golang.org から直接実行できる、動作する自己完結型の実行可能サンプルが含まれています。
わかりやすくて便利。MDNみたいな感じ
フォーマット
Goでは、通常とは異なるアプローチを採用し、ほとんどのフォーマットの問題をマシンに任せます。gofmt プログラム(ソースファイルレベルではなくパッケージレベルで動作する go fmt としても利用可能)は、Goプログラムを読み取り、標準スタイルのインデントと垂直方向の配置でソースを出力し、コメントを保持し、必要に応じて再フォーマットします。
-
gofmt
ないしgo fmt
にフォーマッティングを任せられる-
go fmt
は内部的にgofmt
を使用している
-
- フォーマッタが乱立している言語よりは開発者間のフォーマット差は少なそう?
x<<8 + y<<16
他の言語とは異なり、スペースが意味することを意味します。
ここはよく分からなかった 不要なスペースが入らないなど?
コメント
命名
慣例としてパッケージ名は小文字の単一の単語名にします
他言語のノリで考えていると逆に命名が難しそう できるだけパッケージ単位では単機能であるべきという前提?
長い命名をするとよりも、ドキュメンテーションコメントは、充実されるほうが価値がある場合がよくあります。
命名は短くし、コメントで語れという感じっぽい
外部パッケージから参照可視性は、その最小の文字が大文字かどうかで決まります。
特徴的
慣例としてパッケージ名は小文字の単一の単語名にします。
別の慣例として、パッケージ名はソースディレクトリの名前であるこということです。 src/encoding/base64 にあるパッケージは "encoding/base64" としてインポートされます。名前は base64 であって、 encoding_base64 でも encodingBase64 でもありません。
適切な名前を選択するには、パッケージ構造を使用してください。
長い命名は可読性に役に立ちません。長い命名をするとよりも、ドキュメンテーションコメントは、充実されるほうが価値がある場合がよくあります。
パッケージからのアドレスで要素を表現するぜ!命名は簡潔に!って最近あまり聞かない気がする。(IDEが強いので意味を正確に命名にしようぜ勢が周りには体感多め)
この理論でいくとドメインを切る時は domain
ってだけパッケージ切るよりも domain.user
とかで切って行ったほうが良いのかなーと思った
セミコロン
制御構造(Control structures)
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
ローカル変数のセットアップに使用されるステートメントで参照するが一般的です。
マルチプレクサって何?
select
構文って何?
っていうのを読み進めて知りたい
err は最初のステートメントで宣言されますが、2番目のステートメントでは再割り当てがされます。
ミュータブルだ、、
Goの3つのfor
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
for key, value := range oldMap {
newMap[key] = value
}
// keyのみ取得
for key := range m {
if key.expired() {
delete(m, key)
}
}
// valueのみ取得
sum := 0
for _, value := range array {
sum += value
}
switch
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
breakステートメント
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
ラベルはあまり使わないかもしれない
型のswitch
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
関数
Defer
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
/*
entering: b
in b
entering: a
in a
leaving: a
leaving: b
*/
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
わかりやすい 機能レベルでコードをおまとめできそう クローズ忘れにくそう
データ
new によるアロケーション
new(T) は、型Tの新しい要素にゼロ化されたメモリを割り当て、型*Tの値であるアドレスを返します。
ゼロ値でメモリ確保するぜ!
コンストラクタと複合リテラル
new (File)
と &File{}
は同等で、ローカル変数のアドレスを返す
make によるアロケーション
どうやって使い分けるのが良いのか知りたい
package main
import "fmt"
func main() {
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
fmt.Printf("%v", p)
fmt.Printf("\n-------------\n")
fmt.Printf("%v", v)
}
&[]
-------------
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
配列
- 配列は値です。ある配列を別の配列に割り当てると、すべての要素がコピーされます。
- 特に、関数に配列を渡すと、配列へのポインターではなく配列のコピーを受け取ります。
- 配列のサイズはその型の一部です。型 [10]int と [20]int は区別されます。
package main
import "fmt"
func main() {
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
fmt.Printf("%v", x)
}
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
24.6
スライス
package main
import "fmt"
func main() {
var a = []int{1, 2, 3}
var b = [...]int{1, 2, 3}
fmt.Printf("%v\n", append(a, 4))
fmt.Printf("%v\n", cap(a))
fmt.Printf("%v\n", b)
}
[1 2 3 4]
3
[1 2 3]
二次元スライス
// 二次元配列・スライスの定義
type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
マップ
存在しないキーを検索すると0が返されます。Setのデータ構造は、boolを値として保持するマップとして実装できます。
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
プリント
色々あったけどこれは覚えておく
type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
Append
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
ここで T は任意の型のプレースホルダーです。 Goでは、呼び出し側によって型Tが決定される関数を実際に書くことはできません。
確かジェネリクス出てるはず
初期化
定数
package main
import "fmt"
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
fmt.Printf("%+f\n", b)
fmt.Printf("%+f\n", MB)
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
fmt.Printf("%+f\n", b)
fmt.Printf("%+f\n", KB)
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
func main() {
fmt.Println("Hello, 世界")
fmt.Printf("%+v\n", KB)
fmt.Printf("%+v\n", MB)
fmt.Printf("%+v\n", GB)
fmt.Printf("%+v\n", TB)
fmt.Printf("%+v\n", PB)
fmt.Printf("%+v\n", EB)
fmt.Printf("%+v\n", ZB)
fmt.Printf("%+v\n", YB)
}
Hello, 世界
+1024.000000
+1024.000000
1.00KB
+1048576.000000
+1048576.000000
1.00MB
1.00GB
1.00TB
1.00PB
1.00EB
1.00ZB
1.00YB
変数 || init関数
評価順
定数(ビルド時) -> 変数(実行時に初期化子を入れる) -> init関数
メソッド
インターフェースとその他の型
インターフェース
インターフェース名を知らんと実装できんやん
読む側もインターフェースを知らないとなんで定義しているのかパッと分からないのが微妙な気がする
type Sequence []int
// Methods required by sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Copy returns a copy of the Sequence.
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
s = s.Copy() // Make a copy; don't overwrite argument.
sort.Sort(s)
str := "["
for i, elem := range s { // Loop is O(N²); will fix that in next example.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
変換
式の型を変換して別のメソッドセットにアクセスすることは、Goプログラムのイディオムです。
なんとなく違和感を感じたけど、実装量削減はそれはそうって感じ
今思うと昔Pythonで似たようなことやってたかも
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
????
%#vの時はString()が呼ばれていないみたい、
package main
import (
"fmt"
"sort"
)
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint("123456")
}
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
func main() {
fmt.Println("Hello, 世界")
target := Sequence([]int{1, 2, 3})
fmt.Printf("%#v\n", target)
fmt.Printf("%v\n", target)
}
Hello, 世界
main.Sequence{1, 2, 3}
123456
インターフェースの変換と型アサーション
型アサーションのやり方はこう⇩
package main
import "fmt"
func main() {
var value any
value = "19.0"
r, ok := value.(string)
fmt.Printf("Hello, 世界 %v , %v", r, ok)
}
interfaceを使用してアサート
package main
import "fmt"
type Stringer interface {
String() string
}
type a string
func (a) String() string {
return "abc"
}
func main() {
var ab a = "test"
var value any = ab
switch str := value.(type) {
case string:
fmt.Printf("str %#v", str)
case int:
fmt.Printf("number %#v", str)
case Stringer:
fmt.Printf("stringer %#v", str)
default:
fmt.Println("no match")
}
}
概説
インターフェースを明示的にimpl必要がないのは楽っちゃ楽かもと思ったけど、コードリーディングするときに明示されていないのは不便ではと思った
インターフェースとメソッド
funcに型をつけてる
ただ自分自身を呼ぶだけ
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
ブランク識別子
埋め込み
Embedding なんとなく理解
structに対してinterfaceを埋め込むことで、フィールドが持つ振る舞いを定義する
フィールドは初期化時などに埋めてあげる必要がある
迷宮入り
package main
import "fmt"
type Speak interface {
Say(s string)
}
type baby string
func (baby) Say(s string) {
fmt.Printf("Hey %v", s)
}
type Human interface {
Speak
}
func main() {
var value any = baby("baby")
switch b := value.(type) {
case Speak:
fmt.Printf("BABY %v", b)
suto := &Human{Speak: value}
fmt.Println("Hello, 世界", suto.Speak.Say("Hey"))
default:
fmt.Printf("NOT BABY %v", b)
}
}
並行処理
通信による共有
Do not communicate by sharing memory; instead, share memory by communicating.
Goroutines
ゴルーチンは複数のOSスレッドに多重化され、I/O待ちなど1つがブロックされても他のスレッドが実行され続けます。そのため、スレッドの生成や管理といった複雑な作業は必要ありません。
チャンネル
ちょっと完全に理解した
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Start handlers
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Wait to be told to exit.
}
Channels of channels
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)
Parallelization
Goは平行言語であり、並列言語ではない??のである