この記事は DeNA Advent Calendar 2019 の12/24(火)の記事です。
はじめに
普段は主にPerlを使っていますが、Goを覚えたいのとPythonを復習したいという目的でまとめました。
Pythonの文法はとてもシンプルなので、その差分でGoを覚えるのが早いのではないかと思いました。
なるべく多く書いたつもりですが、いろいろと不足はあると思います。ご留意ください。
コメント
まずはコメントの書き方です。
Pythonにはもともと複数行をコメントアウトする機能はありませんが、プログラム中に文字列を置いても影響がないため、それを利用して複数行コメントを記述することができます。
また、複数行コメントはドキュメンテーション文字列として残すことができます。
# 1行コメント
'''
複数行コメント
'''
"""
複数行コメント
"""
def func():
"""
プログラムの解説など
"""
help(func)
print(func.__doc__)
// 1行コメント
/*
複数行コメント
*/
変数の定義
Pythonは動的型付け言語のため、変数の型を宣言する必要はありません。
n = 10
name = "hoge"
# まとめて定義する
x, y, z = 1, 2, 3
a = b = c = 1
Goの場合は、まず変数名の1文字目に注意すべきです。
- 1文字目が大文字の場合はパッケージ外からアクセスできる
- 1文字目が小文字の場合はパッケージ外からアクセスできない
これは定数・関数にも言える事です。
Goは静的型付け言語だが、明示的な定義と暗黙的な定義が存在します。
明示的な定義
var [変数の名前] [変数の型]
のように定義します。
var n int
n = 1
// まとめて定義する
var x, y, z int
x, y, z = 1, 2, 3
var (
x, y int
name string
)
x, y, name = 1, 2, "hoge"
// 型の宣言と値の代入を同時に行う
var n int = 1
暗黙的な定義
[変数の名前] := [値]
もしくはvar [変数の名前] = [値]
のように定義します。値を代入すると変数の型は暗黙的に推論されます。
n := 1
// varを用いた定義でも型が省略できる
var n = 1
// まとめて定義する
x, y, name := 1, 2, "hoge"
var (
x = 1
y = 2
name = "hoge"
)
定数
Pythonには、定数を定義するためのキーワードはありません。慣例として大文字とアンダーバーのみで定数を表しています。
PI = 3.14
MAX_NUM = 100
Goではconst
を使用して定数を定義します。iota
という識別子を使えば、整数の連番を生成できます。
定数の値を変更しようとするとエラーになります。
const Pi = 3.14
const MaxNum = 100
// ()でまとめて定義する
const (
Pi = 3.14
MaxNum = 100
)
const (
X = iota // 0
Y // 1
Z // 2
)
// 開始番号を指定する場合
const (
X = iota + 10 // 10
Y // 11
Z // 12
)
配列
Pythonの配列(list)は非常にシンプルに書けます。以下は基本的な使い方です。
# 定義
numbers = [1, 2, 3]
# 要素の追加
numbers.append(6)
numbers.insert(3, 5) # numbers: [1, 2, 3, 5, 6]
# 要素数
len(numbers)
# 要素の削除
numbers.remove(3) # numbers: [1, 2, 5, 6]
numbers.pop(1) # numbers: [1, 5, 6]
del numbers[0] # numbers: [5, 6]
# リストを結合
numbers += [3, 4] # numbers: [5, 6, 3, 4]
numbers.extend([1, 2]) # numbers: [5, 6, 3, 4, 1, 2]
# 要素の検索
print(6 in numbers) # True
print(numbers.index(6)) # 1
# リストをソート
numbers.sort() # numbers: [1, 2, 3, 4, 5, 6]
numbers.sort(reverse=True) # numbers: [6, 5, 4, 3, 2, 1]
Goの配列型(array)はサイズの拡張や縮小ができません。Pythonの配列(list)のようなデータ構造は、Goではスライス(slice)に相当します。
スライスの操作でappend
関数がよく使われます。...
については 関数の可変長引数 をご覧ください。
// 配列はサイズが変更できない
array := [3]int{1, 2, 3}
fmt.Println(array[0]) // 1
fmt.Println(array[1:3]) // [2 3]
// スライス
n1 := []int{} // n1: []
n2 := make([]int, 0) // n2: []
numbers := []int{1, 2, 3}
// 要素の追加
numbers = append(numbers, 6) // numbers: [1 2 3 6]
numbers = append(numbers[0:3], append([]int{5}, numbers[3:]...)...) // numbers: [1 2 3 5 6]
// 要素数
len(numbers)
// 要素の削除
numbers = append(numbers[0:2], numbers[3:]...) // numbers: [1 2 5 6]
numbers = numbers[2:] // numbers: [5 6]
// 配列を結合
numbers = append(numbers, []int{3, 4, 1, 2}...) // numbers: [5 6 3 4 1 2]
// 要素の検索
// Pythonのindexに相当するものはないので自分で書く
fmt.Println(IndexOf(numbers, 6)) // 1
func IndexOf(s []int, n int) int {
for i, v := range s {
if n == v {
return i
}
}
return -1
}
// 配列をソート
// sortパッケージを使う
sort.Ints(numbers)
fmt.Println(numbers) // [1 2 3 4 5 6]
sort.Sort(sort.Reverse(sort.IntSlice(numbers)))
fmt.Println(numbers) // [6 5 4 3 2 1]
連想配列
Pythonでは辞書(dictionary)と呼ばれるデータ構造を使います。
# 定義
dic = {'hoge': 1, 'fuga': 2, 'piyo': 3}
list1 = [('hoge', 1), ('fuga', 2), ('piyo', 3)]
dic2 = dict(list1) # dicの値と同じ
dic['hoge']
dic.get('hoge')
# 要素の追加と削除
dic['foo'] = 4
dic.setdefault('bar', 5)
dic.pop('hoge') # {'fuga': 2, 'piyo': 3, 'foo': 4, 'bar': 5}
del dic['fuga'], dic['piyo'] # {'foo': 4, 'bar': 5}
# 要素数
len(dic)
# キーの存在確認
'foo' in dic
# キーと値の取り出し
list(dic.keys()) # ['foo', 'bar']
list(dic.values()) # [4, 5]
for k, v in dic.items():
print(k, v)
Goのマップ(map)はPythonの辞書(dictionary)に相当します。以下の書式で定義します。
map[キーの型]要素の型
// 定義
dic := map[string]int{"hoge": 1, "fuga": 2, "piyo": 3}
dic2 := make(map[string]int)
fmt.Println(dic) // map[fuga:2 hoge:1 piyo:3]
fmt.Println(dic2) // map[]
// 要素の追加と削除
dic["foo"] = 4
delete(dic, "hoge")
fmt.Println(dic) // map[foo:4 fuga:2 piyo:3]
// 要素数
len(dic)
// キーの存在確認
_, exist := dic["foo"]
fmt.Println(exist) // true
if value, exist := dic["foo"]; exist {
fmt.Println(value) // 4
}
// キーと値の取り出し
for k, v := range dic {
fmt.Println(k, v)
}
条件分岐
Pythonにはswitch文がありません。代わりにif... elif... else
を使います。
また、条件式(三項演算子)と呼ばれる書き方があります。
6.12. 条件式 (Conditional Expressions)
https://docs.python.org/ja/3/reference/expressions.html#conditional-expressions
論理演算子はand
,or
,not
を使用します。
x, y = 1, 2
if x > y:
print('x > y')
elif x < y:
print('x < y')
else:
print('x == y')
n = 10
# 条件式
result = "positive" if n > 0 else "negative or zero"
Goの条件分岐は if と switch の2種類があり、簡易文付きifという書き方で、そのブロックだけで有効な変数が定義できます。
三項演算子は存在しませんが、mapでそれっぽい書き方ができます。
論理演算子は&&
,||
,!
を使用します。
x, y := 1, 2
if x > y {
fmt.Println("x > y")
} else if x < y {
fmt.Println("x < y")
} else {
fmt.Println("x == y")
}
# 簡易文付きif
if x, y := 1, 2; x > y {
fmt.Println("x > y")
} else if x < y {
fmt.Println("x < y")
} else {
fmt.Println("x == y")
}
# switch文
x, y := 1, 2;
switch {
case x > y:
fmt.Println("x > y")
case x < y:
fmt.Println("x < y")
default:
fmt.Println("x == y")
}
n := 10
# 三項演算子っぽい書き方
result := map[bool]string{true: "positive", false: "negative"}[n > 0]
ループ
Pythonのループ処理は for や while を使います。
sum = 0
for num in range(1, 11):
sum += num
num, sum = 1, 0
while num <= 10:
sum += num
num += 1
# 無限ループ
num, sum = 1, 0
while True:
sum += num
num += 1
if num > 10:
break
Goのループは for しかありませんが、while のような制御もできます。
sum := 0
for num := 0 ; num <= 10 ; num++ {
sum += num
}
// while
num, sum := 1, 0
for num <= 10 {
sum += num
num++
}
// 無限ループ
num, sum := 1, 0
for {
sum += num
num++
if num > 10 {
break
}
}
関数
Pythonの関数はdef
で定義します。関数の定義が関数呼び出しの実行よりも前に書かなければなりません。
以下のような使い方があります。
- 引数にデフォルト値を持たせることができる
- キーワード引数を使って関数を呼び出すことができる
- 可変長引数を指定することができる
- 複数の戻り値(タプル型)を返すことができる
def greet(name="World"):
print("Hello, " + name)
greet()
greet("Alice")
greet(name="Alice")
# 可変長変数
def greet(*names):
for name in names:
print("Hello, " + name)
greet("Alice", "Bob", "Carol")
# 複数の戻り値
def cal(a, b):
add = a + b
mul = a * b
return add, mul
add, mul = cal(10, 5)
Goの関数はfunc
で定義します。
func [関数名]( [引数の定義] ) [戻り値の型] { [関数の本体] }
デフォルト引数もキーワード引数も存在しませんが、以下のような特徴があります。
- Pythonと同様に複数の戻り値や可変長引数をサポートしている
- 戻り値に名前を予め付けることができる。その場合、returnの後ろに返す値を記述する必要がない
-
defer
キーワードを付けた文は、関数が終了する際に実行される。複数定義した場合は最後の方から呼ばれる
可変長引数
関数の可変長引数は[引数の名前] ...[引数の型]
のように定義します。
そして、可変長引数にスライスを渡す場合は、スライスを展開するために、変数の後に...
をつける必要があります。
func main() {
add, mul := cal(10, 5)
fmt.Println(add, mul) // 15 50
add, mul = calc(10, 5)
fmt.Println(add, mul) // 15 50
greet("Alice", "Bob", "Carol")
names := []string{"Alice", "Bob", "Carol"}
greet(names...) // 可変長引数にスライスを渡す
testDefer() // BDCA
}
// 基本形
func cal(a int, b int) (int, int) {
add := a + b
mul := a * b
return add, mul
}
// 名前付き戻り値
// 引数の型が同じ場合はまとめて書ける
func calc(a, b int) (add int, mul int) {
add = a + b
mul = a * b
return
}
// 戻り値を持たない関数
// 可変長引数
func greet(names ...string) {
for _, name := range names {
fmt.Println("Hello,", name)
}
}
// defer遅延実行
func testDefer() {
defer fmt.Print("A")
fmt.Print("B")
defer fmt.Print("C") // Aより先にCが出力される
fmt.Print("D")
}
例外処理
Pythonは例外をキャッチして処理するにはtry-except
構文を使用します。
def doDivision(x, y):
try:
result = x / y
except Exception as e:
result = None
print("except:" + e.args[0])
else: # 正常終了時に実行する
print("else")
finally: # 終了時に常に実行する
print("finally")
return result
doDivision(10, 2)
# else
# finally
doDivision(10, 0)
# except:test exception
# finally
Goにはtry-except
のような例外機構が存在しません。代わりに、関数の戻り値を複数返せる特性を利用して、エラーが発生したかどうか(errorインターフェース
)を戻り値の一部として返却することによってエラーの検知を実現しています。
errorインターフェース
は以下のように定義されています。
https://golang.org/pkg/builtin/#error
type error interface {
Error() string
}
以下の例ではerrors
パッケージのNew
関数を使ってerror型
を生成しています。
また、defer
を使うことでPythonのfinally
と同じような動きが実現できます。
package main
import (
"fmt"
"errors"
)
func main() {
_, err := doDivision(10, 2)
if (err != nil) {
// エラー処理
}
// defer
_, err = doDivision(10, 0)
if (err != nil) {
// エラー処理
}
// error
// defer
}
func doDivision(i, j int) (result int, err error) {
defer fmt.Println("defer") // 終了時に常に実行する
if j == 0 {
fmt.Println("error")
err = errors.New("Divided by Zero")
return
}
result = i / j
return
}
その他、Goにはpanic/recover
というエラー処理の仕組みもありますが、ここでは割愛します。
クラス
以下は、簡単なPythonクラスの例です。
class Player:
def __init__(self, id, name):
self.id = id
self.name = name
self.__hp = 100
@property
def hp(self):
return self.__hp
def consume_hp(self, num):
self.__hp -= num
player = Player(10001, "Alice")
print(player.hp) # 100
player.consume_hp(10)
print(player.hp) # 90
GoにはPythonで言うところのclass
に相当する構文は存在しませんが、同じような役割として関連のある変数をひとまとめに扱う構造体(struct)が使用されます。
構造体に対してメソッドを定義することができます。メソッドは関数と違ってレシーバーの型とその変数名が必要になります。
以下の例では*Player
というポインタ型に対してconsumeHp
というメソッドを定義しています。
// Player型の構造体
type Player struct{
ID int
Name string
Hp int
}
// コンストラクタ
func newPlayer(id int, name string) Player {
return Player{ID: id, Name: name, Hp: 100}
}
// *Player型のメソッド
func (p *Player) consumeHp(num int) {
p.Hp -= num
}
func main() {
p := newPlayer(10001, "Alice")
fmt.Println(p.Hp) // 100
p.consumeHp(10)
fmt.Println(p.Hp) // 90
}
マルチスレッド
最後は少しだけマルチスレッドについて書きます。
以下はthreading
モジュールを使ってスレッドを生成し、キューでデータを受け渡す簡単な例です。
import threading
import time
from queue import Queue
def worker(a, b, q):
time.sleep(1)
result = a + b
q.put(result) # キューに要素を入れる
print("result:", result)
q = Queue()
thread = threading.Thread(target=worker, args=(2, 3, q))
thread.start()
thread.join()
print("main thread")
result = q.get() # キューから要素を取り出す
q.task_done()
print("received:", result) # received: 5
同じことをGoで実現してみます。
Goでは軽量スレッドであるゴルーチン(goroutine)が並行して動作するように実装されています。go f(x)
と書くと、新たなゴルーチンを起動してその関数を実行します。
ゴルーチンとゴルーチンの間でデータを受け渡しを行うためにチャネル(channel)と呼ばれるデータ構造を使用します。チャネルの型名はchan [データ型]
のように書きます。
package main
import (
"fmt"
"time"
)
func newThread(a, b int, ch chan int) {
time.Sleep(1000)
result := a + b
ch <- result // チャネルにデータを送信
fmt.Println("result:", result)
}
func main() {
ch := make(chan int) // チャネルを生成する
go newThread(2, 3, ch) // 新たなゴルーチンでnewThreadを実行する
fmt.Println("main thread")
result := <-ch // チャネルからデータを受信
close(ch)
fmt.Println("received:", result) // received: 5
}
最後に
Pythonと比較しながらGo言語の文法を見てきました。
Goは静的型付け言語でありながら、Pythonなど動的型付け言語のような書きやすさもあります。
Goは様々な言語から影響を受けていると言われているように、C言語が分かる人ならばGoにおけるポインタや構造体がすぐ理解できるのではないかと思います。
並行処理に重要な goroutine と channel について詳しく書けませんでしたが、また今度書いてみたいと思います。