Formatting

Go言語には、gofmtプログラムが備わっているので、プログラムのformatに時間をかけるなくしています。

なので、reformatをしたいときは gofmt を走らせれば、フォーマットされます。

Commentary

GoにはPHPのように、lineコメントには//、blockコメントには/**/が使われます。/**/はパッケージの担う役割についての説明などに、使います。

— godocコマンドは、Goソースコードのdocumentationを作成します。その際に、packageの一番上のコメントを抜き取って、それをパッケージの詳細情報としてdocument化します。

/*
Package regexp implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

regexp:
concatenation { '|' concatenation }
*/
package regexp

パッケージのコメントを付ける際には、一番上の行は開けて書いてはgodocに認識されないので、ファイルの一行目から書く必要があります。

また、コメントにはgofmtは適用されません。

Name

名前はGo言語内では重要な意味をもちます。

  • packageは小文字で、source directoryの最後の名前によってい決まります。
    例えば、src/encoding/base64にあるpackageは base64が名前です。
    また、package structure(例:fmt.Print()など)を使う際には、長い名前を付けるよりも、簡潔に処理内容を表す名前の方が良いです。
  • Go言語にはgetter/setter 機能はありませんが、次のように付けていくことは可能です。setterはsetOwnerで、getterはOwner(GetOwnerでは無いことに)という名前がつけられます。
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
  • interfaceの名前をつける際は、method name + erが使われることが、よくあります。何故かと言うと、Read, Writerと標準的な名前と区別を付けやすくするためです。ただ、string-converterのような名前と機能が一致しているときには、同じ名前(String)で名前をつけます。
  • 複数の名詞を合わせた名前には_(underScore)より、mixedCapsの方がbetterです。

Control structures

  • if文ではあるパターン(return, break, continue, or goto)のような、次のflowに行かない場合はelseを省略することが可能です。
  • :=で変数宣言されると、本来はerrorを起こす再宣言であったとしても、値が再代入されるだけになります。つまり同じスコープ内の再宣言が:=で可能になるということです。
//It can be possible, not putting error
f, err := os.Open(name)
d, err := f.Stat()
  • array, slice, string, map, reading from a channelといったケースを扱う場合は、for loopより rangeの方が扱いやすくなる事が多いです。
for key, value := range oldMap {
newMap[key] = value
}
  • 文字列の場合、range範囲はUTF-8を解析することによって個々のUnicodeコードポイントを分割して、より多くの機能を果たします。
/*
character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7
*/
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
  • 変数++/ — はstatementであり、expressionではありません。つまりi++を何かの変数に代入はできません。
    したがって、forで複数の変数を実行する場合は、並列代入を使用する必要があります。
func main() {
a := []int{1, 2, 3, 4, 5}

for i, j := 0, len(a) - 1; i < j; i, j = (i + 1), (j -1) {
a[i], a[j] = a[j], a[i]
}
//[5 4 3 2 1]
fmt.Print(a)
}
  • switch文は、interfaceの中にある値を、その値に対応する型タイプで分岐させて処理を行うことができます。
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
}

Functions

  • 関数は複数の値を、返り値にもつことができます。
func (file *File) Write(b []byte) (n int, err error)

上のWrite関数は、file interfaceに追加された関数です。

b != nil の時に、バイト数とnon-nil errorを出します。

また、return value(n int, err error)のように、関数の上の部分で変数宣言を行うことが可能です。n, errの初期値はそれぞれのタイプのzero valueになります。

  • defer文は、mutex.Unlockやファイルのclosing等といった、返り値がどのpathを通ったとしても、必ず何かしらの値が返ってきてほしいときに使われます。

例:

//Contents return the file's contents as a string
func Contents(fName string) (string, error) {
f, err != os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()

var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[:])
result = append(result, buf[:n]...)
if err != nil {
if err == io.EOF {
break
}
return "", err
}
}
return string(result), nil
}

deferを使うメリットは2つあります

  1. file を閉じることを忘れないこと。特に返り値のpathを新しく追加した際などに、よく忘れがちになものを防ぎます。
  2. open処理とclose処理を近くにおけることで、コードの視認性が高まります。

また、次の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

面白いのが、defer文で呼ばれた引数は、defer(遅延)されずにその場で呼ばれていることです。なので次の関数b()は、

func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}

trace(“b”)→fmt.Println()→a()→defer un(“b”)の順に処理されています。

Data

Go言語はnew, makeの2つのprimitive, built-in関数を使ってデータの割当てを行います。2つの処理の動きは異なるものとなります。

New

まず初めに、newはmemory割当てをおこないます。同時に他の言語のようにmemoryの初期化は行われません。

例えば、new(T)は、タイプTの新しい項目にゼロストレージを割り当て、そのアドレス、タイプ* Tの値を返します。 ここでは、新しく割り当てられたTタイプのzero valueへのポインタを返します。

例:

type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer

上の2つの変数(p, v)は、どちらもすぐにSyncedBufferタイプを使用できるような、変数宣言がされています。

また、ゼロ値ではない(zero valueの場合は上の処理)で行いたい場合は次のようにします。

func NewFile(fd int, name string) *File{
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}

new(File) と&File{}は同じ処理になります。また最後の二行は、次のコードと同じ処理内容です。

return &File{fd, name}

明示的に要素を表さずに初期化を行う場合は、そのタイプのzero valueが当てられます。

Make

Make(T, argus)はslice, map, channelにのみ適用される関数です。makeは(zero valueではない)初期化を行い、(ポインター参照*Tではなく)T型の値を返します。

そもそも、slice/map/channelはそれ自体がpointer参照の動きをし、使用する前にデータの初期化が行われる必要があるからです。

例えば次のnew()を使った初期化は、make()を使うとより簡潔に書くことができます。

var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)

逆にいうと、何らかの型タイプのpointer参照をもたせたい場合や、slice/map/channel以外のものを初期化する場合、makeではなくnewが使われます。

Slices

二重構造のスライスを作る場合 、2つの方法があります。

  1. 各スライスを独立して割り当てる(allocate)ことです。
  2. 1つの配列を割り当て、個々のスライスをその配列にポイントすることです。

スライスが拡大または縮小する可能性がある場合は、次の行を上書きしないように、スライスを個別に割り当てる1つ目のアプローチが良いです。

そうでない場合は、単一の割り当てでオブジェクトを構築する二つ目の方が効率的です。

一つ目のアプローチ:

// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize)
}

二つ目のアプローチ:

picture := make([][]uint8, YSize) // One row per unit of y.pixels := make([]uint8, XSize*YSize) 
picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

Printing

print文を使う場合、fmt.Printlnのように、すべてfmtパッケージからアクセスします。書式の文字列を指定する必要はなく、それぞれ規定の処理を行う関数があり、処理にあったものを使っていきます。

次の関数は全て同じ文字列を返します。

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

また、整数の小数点(decimal)といった、デフォルトの変換だけが必要な場合は、キャッチオール形式%v( value)を使用します。 結果はPrintとPrintlnが生成するものとまったく同じです。

さらに、このフォーマット(%v)は、配列、スライス、構造体、マップなどのあらゆる値をprintすることができます。

主要なフォーマットは

  • %v 通常時の値のフォーマット。どんな値でも適用可能。
  • %#v go-syntax情報全てを表示する。
  • %T 値の型タイプを表示する。

さらに、Tタイプに次のような関数String() stringを追加することで、default formatを制御して、必要な情報をとってくることが可能になります。

type T struct {
a int
b float64
c string
}
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, string(t.c))
}
func main() {
t := new(T)
t.a = 23
//23/0/""
fmt.Printf("%v\n", t)
}

また、このString()関数の引数をそのまま使う場合、終わらない再帰関数になる事があるので、 string(t.c) のように文字列に変換する必要があります。

Initialization

定数(const)は関数のローカル内で定義されたとしても、コンパイル時に作成されます。また定数には、number, character, boolean, stringのみが使用可能となります。

更には、列挙定数はiota列挙子を使用して作成されます。 iotaは式の一部であり、式を暗黙的に繰り返すことができるため、複雑な値のセットを簡単に作成できます。

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)
// ans so on

main関数側で表示します。

func main() {
b := ByteSize(KB)
fmt.Println(b)
}

ちなみに、const内で使われている<<(Arithmetic operators)は下のサイトに行くと、分かりやすい解説が見れます。

また、変数宣言として表現することができない初期化を行うような、init関数の主な役割は、実際の実行が始まる前にプログラム状態の正当性を検証、修復することです。どういうことなのか、次のコードを見ていきます。

例:

func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

また、init関数はimportされたパッケージの初期化が全て終わってから、処理が行われています。

感想

このEffective Goを通して、A Tour of Goでやってきた事の整理を、どういう処理が行われているかを少しづつ理解してきました。

ただし、documentを見てmethodや関数を理解して使えるかは、まだ少し厳しいと思いました。

特に次の内容channelやconcurrency(並行プログラミング)の概念は、かなりの鬼門なので、気合を入れていきたいと思います。

次回は、interfaceやchannel、concurrencyといったGo言語らしい機能についてまとめていきます。

--

--