Open114

[輪読]Effective Goを読んでいく

itsukiyitsukiy

イントロダクション

このドキュメントは明確で慣用的なGoのコードを書くためのヒントを示します。言語仕様 や Tour of Go や How to Write Go Code を補強するもので、最初に読むべきものです。

Goをやっていくための一歩目として間違ってなさそうだったので迷わず読んでいけそう

多くのパッケージでは golang.org から直接実行できる、動作する自己完結型の実行可能サンプルが含まれています。

わかりやすくて便利。MDNみたいな感じ

RixiaRixia

フォーマット

Goでは、通常とは異なるアプローチを採用し、ほとんどのフォーマットの問題をマシンに任せます。gofmt プログラム(ソースファイルレベルではなくパッケージレベルで動作する go fmt としても利用可能)は、Goプログラムを読み取り、標準スタイルのインデントと垂直方向の配置でソースを出力し、コメントを保持し、必要に応じて再フォーマットします。

  • gofmtないしgo fmtにフォーマッティングを任せられる
    • go fmtは内部的にgofmtを使用している
  • フォーマッタが乱立している言語よりは開発者間のフォーマット差は少なそう?

x<<8 + y<<16
他の言語とは異なり、スペースが意味することを意味します。

ここはよく分からなかった 不要なスペースが入らないなど?

itsukiyitsukiy

コメント

itsukiyitsukiy

/* */ でブロックコメント、 / / で行コメントを書く

itsukiyitsukiy

godocはパッケージコメントを適切に書いたりしてあげるといい感じに見せてくれる。
書いてみないと分からなそう

itsukiyitsukiy

すべてのdocコメントが説明する項目の名前で始まる場合、goツールのdocサブコマンドを使用して、grepを介して出力を実行できます。

パッケージを利用するときに知っておくとちょっとだけ便利??

go doc -all regexp | grep -i parse
itsukiyitsukiy

命名

RixiaRixia

慣例としてパッケージ名は小文字の単一の単語名にします

他言語のノリで考えていると逆に命名が難しそう できるだけパッケージ単位では単機能であるべきという前提?

RixiaRixia

長い命名をするとよりも、ドキュメンテーションコメントは、充実されるほうが価値がある場合がよくあります。

命名は短くし、コメントで語れという感じっぽい

itsukiyitsukiy

外部パッケージから参照可視性は、その最小の文字が大文字かどうかで決まります。

特徴的

慣例としてパッケージ名は小文字の単一の単語名にします。

別の慣例として、パッケージ名はソースディレクトリの名前であるこということです。 src/encoding/base64 にあるパッケージは "encoding/base64" としてインポートされます。名前は base64 であって、 encoding_base64 でも encodingBase64 でもありません。

適切な名前を選択するには、パッケージ構造を使用してください。

長い命名は可読性に役に立ちません。長い命名をするとよりも、ドキュメンテーションコメントは、充実されるほうが価値がある場合がよくあります。

パッケージからのアドレスで要素を表現するぜ!命名は簡潔に!って最近あまり聞かない気がする。(IDEが強いので意味を正確に命名にしようぜ勢が周りには体感多め)

この理論でいくとドメインを切る時は domain ってだけパッケージ切るよりも domain.user とかで切って行ったほうが良いのかなーと思った

RixiaRixia

セミコロン

itsukiyitsukiy

字句解析器が単純なルールを使用して解析時にセミコロンを自動的に挿入するため、ソースコードにはほとんどセミコロンが含まれていません。

そうなんですか〜って感じ。個人的にはセミコロンあったほうが好き

itsukiyitsukiy
go func() { for { dst <- <-src } }()

これ分からんけど、読んでたらわかるようになるでしょう

itsukiyitsukiy

制御構造(Control structures)

itsukiyitsukiy
if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

ローカル変数のセットアップに使用されるステートメントで参照するが一般的です。

itsukiyitsukiy

マルチプレクサって何?
select 構文って何?
っていうのを読み進めて知りたい

itsukiyitsukiy

err は最初のステートメントで宣言されますが、2番目のステートメントでは再割り当てがされます。

ミュータブルだ、、

itsukiyitsukiy

Goの3つのfor

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }
itsukiyitsukiy
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
}
itsukiyitsukiy
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
}
itsukiyitsukiy

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)
        }
    }
itsukiyitsukiy
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
}
RixiaRixia

関数

itsukiyitsukiy
func (file *File) Write(b []byte) (n int, err error)

ここまでできたけど、途中でエラーが起きたぜっていうのを表現できるのは嬉しそう。。なのか??

itsukiyitsukiy
func nextInt(b []byte, pos int) (value, nextPos int) {

違和感しかないんだけど、使ったほうが良いのか?

RixiaRixia

Defer

itsukiyitsukiy
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
*/
RixiaRixia
 f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close() 

わかりやすい 機能レベルでコードをおまとめできそう クローズ忘れにくそう

itsukiyitsukiy

データ

itsukiyitsukiy

new(T) は、型Tの新しい要素にゼロ化されたメモリを割り当て、型*Tの値であるアドレスを返します。

ゼロ値でメモリ確保するぜ!

itsukiyitsukiy

new (File)&File{} は同等で、ローカル変数のアドレスを返す

itsukiyitsukiy
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]
itsukiyitsukiy
  • 配列は値です。ある配列を別の配列に割り当てると、すべての要素がコピーされます。
  • 特に、関数に配列を渡すと、配列へのポインターではなく配列のコピーを受け取ります。
  • 配列のサイズはその型の一部です。型 [10]int と [20]int は区別されます。
itsukiyitsukiy
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
itsukiyitsukiy
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]
itsukiyitsukiy
// 二次元配列・スライスの定義
type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.
itsukiyitsukiy

存在しないキーを検索すると0が返されます。Setのデータ構造は、boolを値として保持するマップとして実装できます。

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}
itsukiyitsukiy

色々あったけどこれは覚えておく

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}
itsukiyitsukiy
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

ここで T は任意の型のプレースホルダーです。 Goでは、呼び出し側によって型Tが決定される関数を実際に書くことはできません。

確かジェネリクス出てるはず

RixiaRixia

初期化

itsukiyitsukiy
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
itsukiyitsukiy

評価順
定数(ビルド時) -> 変数(実行時に初期化子を入れる) -> init関数

itsukiyitsukiy

メソッド

itsukiyitsukiy
func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Again as above.
    *p = slice
    return len(data), nil
}

ByteSlice のアドレスを関数に渡すことができます。これは *ByteSlice 型が io.Writer インターフェースを満たすためです

多分インターフェースを満たしとけばポインタメソッド名はなんでも良さそう。Pythonのダックタイピングを思い出した
多分ダメそう

RixiaRixia

インターフェースとその他の型

itsukiyitsukiy

インターフェース名を知らんと実装できんやん
読む側もインターフェースを知らないとなんで定義しているのかパッと分からないのが微妙な気がする

itsukiyitsukiy
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 + "]"
}
itsukiyitsukiy

式の型を変換して別のメソッドセットにアクセスすることは、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))
}
itsukiyitsukiy

????
%#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
itsukiyitsukiy

型アサーションのやり方はこう⇩

package main

import "fmt"

func main() {
	var value any
	value = "19.0"
	r, ok := value.(string)
	fmt.Printf("Hello, 世界 %v , %v", r, ok)
}
itsukiyitsukiy

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")
	}
}
itsukiyitsukiy

インターフェースを明示的にimpl必要がないのは楽っちゃ楽かもと思ったけど、コードリーディングするときに明示されていないのは不便ではと思った

itsukiyitsukiy

ブランク識別子

RixiaRixia

慣習により、インポートエラーを抑えるためのグローバル宣言はインポート文のすぐ後に記述します。

itsukiyitsukiy

これを書けるのはGoを知っている人だ
書いてあるとコードリーディングが少ししやすそう

 var _ json.Marshaler = (*RawMessage)(nil)
itsukiyitsukiy

埋め込み

itsukiyitsukiy

Embedding なんとなく理解
structに対してinterfaceを埋め込むことで、フィールドが持つ振る舞いを定義する
フィールドは初期化時などに埋めてあげる必要がある

itsukiyitsukiy

迷宮入り

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)
	}
}
RixiaRixia

並行処理

itsukiyitsukiy

Do not communicate by sharing memory; instead, share memory by communicating.

itsukiyitsukiy

ゴルーチンは複数のOSスレッドに多重化され、I/O待ちなど1つがブロックされても他のスレッドが実行され続けます。そのため、スレッドの生成や管理といった複雑な作業は必要ありません。

itsukiyitsukiy

ちょっと完全に理解した

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.
}
itsukiyitsukiy
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)
itsukiyitsukiy

Goは平行言語であり、並列言語ではない??のである