📝

Java経験ありのRubyistからGoへ入門するときの差分メモ(更新中)

2023/02/16に公開

前置き

Java(2年)→Ruby(5年)のエンジニアがGoに入門する時の、Go独特だなあと思ったとこだけメモ
A Tour of Goを順に勉強、いろんなブログを参考にさせていただいています🫶

基礎

コメントアウト

//
/* */

大文字と小文字の区別(外部パッケージからアクセスできるのは大文字はじまりのみ)

関数名、型変数名、フィールド名は大文字の場合、他のパッケージからアクセス可能になる

package import

factored(グループ化) import statement

import "fmt"
import "math"

// の代わりに

import (
	"fmt"
	"math"
)

Functions

引数は変数名 型名の順番

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

関数の2つ以上の引数が同じ型である場合には、最後の型を残して省略可能

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

複数の戻り値を返せる

rubyと一緒
返り値の型は一緒の場合でも、名前をつけない場合(すぐ下参照)は省略できな様子

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

naked return(名付けて返却 named return values)

戻り値となる変数に名前をつける( named return value )ことができます。
戻り値に名前をつけると、関数の最初で定義した変数名として扱われます。
return には何も指定しない
複数返却値がある場合、型は省略できる

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

func main() {
	fmt.Println(split(17))
}
// => 7 10

変数宣言

パッケージ内 or 関数ないで宣言できる
var 変数名 型 の順番
複数まとめて宣言もできる
初期化子(初期化のデフォルト値)がある場合は、型を省略できる

var c, python, java bool
var i, j int = 1, 2

func main() {
    var i int
    var c, python, java = true, false, "no!"
    fmt.Println(i, j, c, python, java)
}

さらに関数内では、varと型を省略してかける

func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"

	fmt.Println(i, j, k, c, python, java)
}

まとめて宣言可能

var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)

基本型

bool

string

// サイズを指定する特別な理由がない or 整数の変数が必要な場合はintを使う
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 の別名

rune // int32 の別名
     // Unicode のコードポイントを表す

float32 float64

complex64 complex128

★★★ゼロ値★★★

変数に初期値を与えずに宣言すると、ゼロ値( zero value )が与えらる
言語によって分かれるところ

数値型(int,floatなど): 0
bool型: false
string型: "" (空文字列( empty string ))

func main() {
	var i int
	var f float64
	var b bool
	var s string
	fmt.Printf("%v %v %v %q\n", i, f, b, s)
}
// => 0 0 false ""

型変換

Cとは違い、Goの型変換は明示的な変換が必要

i := 42
f := float64(i)
u := uint(f)

※型の確認
fmt.Printf("%T\n", s)

定数

※大文字を使うのがベストプラクティス

const キーワードを使う
文字(character)、文字列(string)、boolean、数値(numeric)のみで使える
:= を使って宣言できない
型付きの状態でも、型付きでない状態でも宣言できる
→numericの型のない定数は、状況によって必要な型をとることができる

const Pi = 3.14 // 型付きのvariableに代入できない
const Pu int = 2 // かたが固定される。型付きのvariableに代入できない

func main() {
	const World = "世界"
	fmt.Println("Hello", World)
	fmt.Println("Happy", Pi, "Day")

	const Truth = true
	fmt.Println("Go rules?", Truth)
}

シングルクォートでstringは囲めない、囲んだらRune型になる

文字列は"(ダブルクォート) 、もしくは`で囲むで囲む
バッククォートで囲むとそのまま改行できる。

func main() {
	fmt.Println("プログラミング言語では、\nGoよりもPythonが好きです。") // \nで改行
	fmt.Println(`プログラミング言語では、
GoよりもPythonが好きです。`)

}

Flow control statement

whileを兼ねたfor

forの後に()はつけない。{}はつける。
初期化ステートメント(i)はfor文の中でのみ有効
初期化と後処理は省略可能
;も省略可能

// フルversion
func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}

// 初期化と後処理を省略
func main() {
	sum := 1
	for ; sum < 1000; {
		sum += sum
	}
	fmt.Println(sum)
}

// ;省略。whileと同じ。
func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}

// 無限ループ
func main(){
   for {
   }
}

ifは条件の前に評価式を書ける

// 基本計
if 条件式A {
		処理コードA
	} else if 条件式B {
		処理コードB
	} else {
		処理コードC
	}

// ここでいうvはif(else含む)のスコープ内でのみ有効
func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	}
	return lim
}

switch

条件に合致したらその時点で抜ける = 自動break

// 条件あり
func main() {
	fmt.Println("When's Saturday?")
	today := time.Now().Weekday()
	
	switch time.Saturday {
	case today + 0:
		fmt.Println("Today.")
	case today + 1:
		fmt.Println("Tomorrow.")
	case today + 2:
		fmt.Println("In two days.")
	default:
		fmt.Println("Too far away.")
	}
}

// 条件なし
t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}

defer: 遅延実行

評価はされるが、関数の呼び出し自体は、呼び出し元の関数がreturnするまで実行されない
複数ある場合はLIFO(last-in-first-out)
よくある使い道としては、cleanup処理をエラー時でも漏れなく実行するときがある

func main() {
  defer fmt.Println("最後に実行される")
  defer fmt.Println("3番目に実行される")
  defer fmt.Println("2番目に実行される")

  fmt.Println("最初に実行される")
}
func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()
    // 次のos.Createでエラーになってもちゃんと Closeが実行される
    
    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

ポインタ

&変数名で、ポインタを引き出せる
*ポインタで、ポインタにある値を取り出せる

i := 42
p = &i
fmt.Println(p) // 0xc00001c030 pointerのaddress
fmt.Println(*p) //42 pointerが指し示す値

*p = 21
fmt.Println(*p) //ポインタを通して値を設定できる(dereferencing, indirectingと呼ばれる)

Struct

RubyやJavaと同じ、ただのfieldの集まり

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
	
	p := &v
	p.X = 100 // (*p).X これの省略形
	fmt.Println(v)
	
	v2 = Vertex{X: 1}  // {1 0} フィールドの一部だけ指定もできる
}

Array

[n]T n個のT型のarray
サイズは固定
使いずらい?安心してください、sliceというものがいます

func main() {
	var a [2]string
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a[0], a[1])
	fmt.Println(a)

	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)
}

★★★Slice★★★

書き方

ざっくり説明だと、可変長なArray。
ちゃんとした説明だとArrayのポインタをフィールドに持つ構造体。
定義はこう

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}

Slice自体にはどんなデータも格納しておらず、単に元の配列の部分列を指し示している。
=Slice は内部に持ったArrayを len フィールドの長さに切り出すことで 可変であるように振る舞う。

★参照渡し
スライスの要素を変更すると、その元となる配列の対応する要素が変更される
同じ元となる配列を共有している他のスライスは、それらの変更が反映される。
ちなみに配列は値型なので、参照渡しをしなければコピーが作られてしまい非効率。sliceを使う。

[]T でT型のslice
※arrayは[n]T
a[1:4] aのindex1から3の要素を含むスライス。4は含まないのがポイント

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s) // [3 5 7] index1からindex3まで
}

スライスは長さのない配列リテラルのようなもの

// これは
[]bool{true, true, false}
// この配列を作って、それを参照するスライスを作成している
[3]bool{true, true, false}

上限下限の省略

新たなスライスが作成される

a[0:10]
a[:10]
a[0:]
a[:]

capacity

どういうことかというと、lenはスライスの長さ(論理的な長さ)で、capacityは配列の長さ(物理的な長さ)。

スライスの要素数が配列を下回っている場合は配列をそのまま利用できる。しかし、スライスの要素数が配列を超える場合は配列自体を作り直す必要がある。 これをアロケーションという。

わかりやすいブログ記事

スライスのゼロ値はnil

printすると[]だけど

func main() {
	var s []int
	s := []int // これはエラー
	s := []int{} //これはいける
	fmt.Println(s, len(s), cap(s)) // [] 0 0 
	if s == nil {
		fmt.Println("nil!")
	}
}

make & append関数

make([]T, 初期length, キャパシティ)
append([]T, 追加する変数 ...)
長さがキャパを越えるとエラー
しかしappendは容量を超えてできてしまう。これはアロケーションが発生し、別のスライスが作られているからできる。なのでappendは左辺で受け取る

value := make([]int, 6, 6)
fmt.Println(value)
// 出力結果 [0, 0, 0, 0, 0, 0]

value := make([]int, 8, 6)
fmt.Println(value)
// エラー

s := make([]int, 1, 2)
fmt.Printf("%d (%p)\n", s, s) // [0] (0x40e020)
s = append(s, 1)
fmt.Printf("%d (%p)\n", s, s) // [0 1] (0x40e020)
s = append(s, 2)
fmt.Printf("%d (%p)\n", s, s) // [0 1 2] (0x40e050) 別物になっている

slices of slice (連想配列)

	board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

	fmt.Println(board)

	// The players take turns.
	board[0][0] = "X"
	board[2][2] = "O"
	board[1][2] = "X"
	board[1][0] = "O"
	board[0][2] = "X"

Rangeとfor (Rubyでいうところのeach)

ループ毎にindexとvalueを返す

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}

// indexやvalueを捨てたり、省略もできる
func main() {
	pow := make([]int, 10)
	for i := range pow { //省略
		pow[i] = 1 << uint(i) // == 2**i
	} 
	
	// indexもvalueも呂方捨てる
	for range pow {
		fmt.Println("hoge") // iもvも省略
	}
	
	// ※ indexだけ捨てることはできないので、valueだけ欲しい場合は、iは_で捨てる
	for _, value := range pow {
		fmt.Printf("%d\n", value)
	}
}

Map

key-value型、rubyでいうhash
map[string]Vertex map[keyのT]valueのT

ゼロ値はnil。 nil マップはキーを持っておらず、またキーを追加することもできない。
make 関数は指定された型のマップを初期化して、使用可能な状態で返す。
=> 作成するときはmake関数を必ず使う

	var m map[string]int
	fmt.Printf("%p %T %v\n", m, m, m)
	// => 0x0 場所がメモリ上に確保されていない
	// 要素を追加しようとすると、panic: assignment to entry in nil map
	
	var m2 map[string]int = make(map[string]int)
	fmt.Printf("%p %T %v", m2, m2, m2)
	// => 0xc0000981b0 makeで作った方は追加できる
type Vertex struct {
	Lat, Long float64
}
// keyを指定して作成
var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

取得、削除

更新: m[key] = elem
取得: elem = m[key]
削除: delete(m, key)

存在確認 ok

elem, ok = m[key]

	m["Answer"] = 48
	fmt.Println("The value:", m["Answer"])

	delete(m, "Answer")
	fmt.Println("The value:", m["Answer"])

	v, ok := m["Answer"]
	fmt.Println("The value:", v, "Present?", ok)
	// => false

increment、decrement

式 (expression) ではなく文 (statement) です
expression: 評価された値を持つ
statement: 手続き

a := 2
fmt.Println(a++) // syntax error: unexpected ++ in argument list
b := a++ // syntax error: unexpected ++ at end of statement

// こう書く
	a := 2
	a++
	b := a
	fmt.Println(b) // =>3

関数も変数

// 引数に関数を取るパターン
func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))

	fmt.Println(compute(hypot))
	fmt.Println(compute(math.Pow))
}

//関数を返すパターン
func foo() func() int {
	return func() int {
		return 2020
	}
}

func main() {
	x := foo()
	fmt.Println(x())
	
	fmt.Println(foo()())
}

Blocks

スコープを区切るために、{ } で囲う技法
{}ブロックの中の変数はその中でしか利用できない

func main() {
	v := "123"
	{
		v := "223"
		fmt.Println(v) // 223
	}
	fmt.Println(v) // 123
}

Goの関数はクロージャー

改めてクロージャーとは、関数と、関数内でアクセスできる変数をセットで持ち合わせているオブジェクト
(Rubyのクラスとクラス変数みたいなオブジェクト。blocksの応用版)

Points

  • 関数の戻り値が関数であること (func() int) で戻り値を指定
  • 戻り値の関数を無名関数で作成
  • 関数の戻り値が関数なので、変数に代入して使用すること
  • 関数を実行するには、関数を代入した変数で実行すること

役割
変数のスコープを制限できる = 変数を関数を通してしか変更できないようにしているため、カプセル化に一役買っている

// adderは引数なしで、intを引数に取りintを返す関数を返す関数
func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	// 変数に代入され、クロージャーになる
	// posとnegは別物 = sumはクロージャーごとに違う
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}

// ここでadder()の中身はこう
// sumの初期化はないので、前回の実行結果を引き継げる
func(x int) int {
		sum += x
		return sum
	}

メソッド(型にメソッドを定義する)

Goにはクラスはないが、型にメソッドを定義できる
レシーバ引数(関数名の前に定義)として型をメソッドに定義する
メソッド = レシーバ引数を伴う関数

レシーバを伴うメソッドの宣言は、レシーバ型が同じパッケージにある必要あり。つまり組み込み型にもメソッド定義はできない

変数レシーバ

変数をレシーバ引数に入れるパターン
変数レシーバの場合、引数に渡す変数はコピーされて渡しているため、レシーバに入れた変数をメソッド内で変更できない
そのためポインタレシーバより一般的ではない

type Vertex struct {
	X, Y float64
}

// 関数名の前に、メソッドを紐づけたい型をレシーバ引数にとる
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

// 任意の型にも定義できる
type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

ポインタレシーバ

ポインタをレシーバ引数に入れる
メリット

  1. メソッドがレシーバが指す先の変数を変更するため。
  2. メソッドの呼び出し毎に変数のコピーを避けるためです。 例えば、レシーバが大きな構造体である場合に効率的。
func (v Vertex) Abs() float64 {
	fmt.Println(v.X)
	fmt.Println(v.Y)
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// *をつけてポインタにメソッドを定義する
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f // 3 -> 30に変更される
	v.Y = v.Y * f // 4 -> 40に変更される
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

ポインタレシーバの場合、レシーバーが変数でもポインタレシーバとして処理される(その逆も然り)

その逆も然りで、変数レシーバとして定義されたメソッドに、ポインタレシーバを渡しても返還される

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func AbsFunc(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func ScaleFunc(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	// Scaleはポインタレシーバだが、
	// 変数をレシーバにとってもポインタに変換されて呼び出される
	v.Scale(2) 
	// 普通の関数を呼び出すときは、&をつけてポインタにする
	ScaleFunc(&v, 10)
	
	// 王道にポインタレシーバメソッドにポインタレシーバ
	p := &Vertex{4, 3}
	p.Scale(3)
	ScaleFunc(p, 8)
	
	p := &Vertex{4, 3}
	// ポインタが変数レシーバに変換される
	fmt.Println(p.Abs())
	// 普通の関数はちゃんと渡さないとだめ
	fmt.Println(AbsFunc(*p))
}

型の宣言(type)

type 識別子 型
使い方

  1. 独自の型を宣言する
  2. 既存の型に新しい名前をつける
// 構造体型の宣言
type Hoge struct {
    // フィールドリスト
}

// インタフェース型の宣言
type Fuga interface {
    // メソッドリスト
}

// 関数も型にできる
type HandlerFunc func(w http.ResponseWriter, r *http.Request)

// sliceなど、型リテラルもいける
type slice_type []int

// 組み込み型もいける。ただしHexはint型ではないので、intと演算するときはint型への変換が必要
type Hex int

interface

インタフェース型の宣言時に指定したメソッドリストのメソッドをすべて実装することで,インタフェースを実装することができる
implementsなどを使って明示的に実装する必要はない

インターフェースを実装すると、インターフェースとして振る舞うことができる = インタフェースの型の変数に代入したり,引数として関数に渡すことができることを指す

// 定義の仕方
type TypeName interface {
    // メソッドリスト
    Method1()
    Method2()
}

// Stringメソッドを持ったStringer型がある
type Stringer interface {
    String() string
}

// Hex型を作る
type Hex int

// HexにStringメソッドを実装する
func (h Hex) String() string {
    return fmt.Sprintf("0x%x", int(h))
}

// StringerとしてHexは振る舞える
func main {
	var stringer fmt.Stringer
	// int型の100をHex型にキャストし,fmt.Stringer型の変数に代入している
	stringer = Hex(100)
}

空のinterface

interface{}型は,メソッドリストがないインタフェース型の型リテラル
メソッドリストがない = メソッドを一つも実装しなくても,interface{}インタフェースを実装したことになる
interface{}型の変数や引数には,どんな型の値でも代入したり,渡したりすることができる

Discussion