この記事はNTTテクノクロス Advent Calendar 2025 4日目の投稿記事になります。
NTTテクノクロスの松永です。アドベントカレンダーイベント初参加となります!
今回は、Pythonを中心に触れてきた人間が、たまたまGolang(以下Go)を学んでみて何を感じたのか、以下のポイントでまとめてみようと思います。
- たくさんの観点があると思いますが、基本的な文法な中心に見たいと思います
- どのような点で「違い」や「これはなんだ?」と思ったか
- Python経験者目線で、上記のような違いなどをどのように解釈しようとしたか
- それぞれの言語に対してどのようなイメージを抱いたか
Pythonを触っていて他の言語も触れてみたいという人の参考になればと思います。
自己紹介
当社にはキャリア採用で入社し、現在は生保金融系業界の企業で大規模なプロジェクトのPMOとして、現場の業務システム開発を勉強中です。
当社に入社する以前は、Pythonを中心に業務自動化関連のスクリプトやWebアプリの開発をしたり、DBの設計したり、AWSをいろいろさわったりなど、3年ほど幅広くいろいろなことをやっていました。
Pythonでの業務経験
- 業務で触ったことのあるPythonのバージョンは3.10~3.12
- Seleniumを使ったブラウザ自動操作やBeatifulSoupを使ったHTML解析など
- Pandasを使った簡単なデータ操作、成形など
- Django4.2、DRFを使ったWebアプリ開発
- FastAPIを利用したWebAPI開発
なぜGo?
結論から言うと、友人が勧めてくれた、静的型付け言語だった、という単純な理由です。
もう少し詳しく言えば、
- 昔からの友人がGoを使って業務しており、「Go楽しいよ!Python遅い」という感じで推されたため
- Pythonで型に関するあれこれ(標準のtypingや、pydanticなど)を使うように意識し始めていたため。静的型付け言語をちゃんと知っておいたほうがなんとなくいいのではと思っていた
特に2番目の理由は、VSCodeでせっかくPylanceがいろいろサポートしてくれている状態なら、できる限りコードをカラフルにしたいなという半分冗談のような気持ちでした。真面目な目線では、人に説明する・教える際、これはどんなデータの形なのか目に見えないとわかりづらいというのもありました。
という感じで、Goについてはgoogleが作ったとか、静的型付け言語だということくらいしか事前知識はありませんでした。(ちょっと調べてみると、構文もシンプルらしいし…この手の話は実際と乖離していることもありますが)
学習法
- Tour of Go : まずはここから、無料で手軽に始められるので(ただ後半めちゃくちゃ難しくないか??と思いながら)
- 初めてのGo言語(第1版) : オライリーから出ている書籍で、他言語経験者も意識したものになっています、今年第2版が出たようです
- ChatGPT : 具体例がほしくなったときに問題を出してもらっていました
上記でだらだらとトータル3~4ヶ月くらいやっていたと思います。
実際に触ってみて感じた違い
基本的な文法
型定義
Goは静的型付け言語ということもあり、型定義はpythonの型推定とは違います。ただし、goにも型推定はあります。この部分はpythonが(通常は)型推定のみにとどまるのに対して、goは使い分けることもできるという点で意識が必要と感じました。
# 3.12以降
# あくまでもエイリアス
type UserID = int
type UserName = str
def search_user(id:UserID) -> UserName:
# 何かしら処理
// 型を定義する
type UserID int
type UserName string
var id UserID = 1234
var myID int = id //型が異なるのでコンパイルできない
func SearchUser (userId UserID) UserName {
// 何かしら処理
}
// 型エイリアスにとどめておく
type UserID = int
type UserName = string
var id UserID = 1234
var myID int = id
func SearchUser (userId UserID) UserName {
// 何かしら処理
}
コンパイル時にエラーがでるなど、最初は??となりますが、Goが明示的な状態を目指すことを理解していくとこのあたりもすんなり入ってきます。
データ型
細かいところをあげればどちらにもたくさんのデータの型がありますが、コレクション型やユーザー定義型などについて触れたいと思います。
コレクション型
Pythonではリスト、辞書、セットあたりを、Goでは配列、スライス、マップあたりのことです。
Pythonではリストを使う際、最初から要素数などを考慮することはあまりなかったような気がしますし、データの追加(または削除)も気軽にやっていた気がします。この感覚で初めてGoの配列を知った時に、データサイズの宣言が必要、要素は不変という条件に戸惑いました。が、スライスが直後に現れることでこの問題も解決していきます。
pets = ["pochi", "tama"]
lucky_number = [7, 111, 777]
pets.append("taro")
print(pets)
# ["pochi", "tama", "taro"]
# リスト内リスト
matrix = [
# リスト内リストの型が違ってもOK
["pochi", "tama"],
[7, 111, 777]
]
var pets_arr = [2]string{"pochi", "tama"}
func main(){
// NG
pets.append("taro")
}
var petsSlice = []string{"pochi", "tama"}
func main() {
petsSlice = append(petsSlice, "taro")
fmt.Println(petsSlice)
// [pochi tama taro]
}
// スライス内スライス
var petsSlice = [][]string{
{"pochi", "tama"},
}
func main() {
petAnother := []string{"jiro", "mike"}
petsSlice = append(petsSlice, petAnother)
fmt.Println(petsSlice)
// [[pochi tama] [jiro mike]]
// 型が異なるため不可
luckyNumber := []int{7, 111, 777}
petsSlice = append(petsSlice, luckyNumber)
}
// make関数でサイズを決めてスライスも作れる
petsSlice := make([][]string, 3)
実際に書いてみると、Goの場合はそれなりに手間がかかります。リスト型やスライスはそれなりに使うデータ型である手前Pythonのほうが断然楽ではあるものの、違う型の要素を持たせることのできないGoの安全性という部分はかなりありがたいと感じます。
persons = {
"Bob":21,
"John":33
}
persons_complex = {
TeamA:[
"Bob",
"John"
]
TeamB:[
"Alexandra",
"Naomi"
]
}
func main() {
persons := map[string]int{}
persons["Bob"] = 21;
persons["John"] = 33;
}
func main() {
personsComplex := map[string][]string {
"TeamA": []string {"Bob", "John"},
"TeamB": []string {"Alexsandra", "Naomi"},
}
}
辞書/mapとなると明らかにGoの記述の煩雑さが目立つようになってきます。マップのvalueにスライスを持ってきたいとき場合など、ぱっと見確認したくなくなってしまいそうです…このあたりは安全性とのトレードオフというか、Pythonのシンプルさが強みとしてでている部分だなと感じます(JSONの扱いなども同じくかもしれません)
ポインタ
Goの特徴的なデータ型である構造体について触れる前に、データの受け渡しにも関わってくるポインタについても触れておきます。ポインタについては難しいというイメージがありましたが、Goのポインタは、Pythonのようにポインタのない言語から来た人間にも親切な印象があります(C言語とか難しいのですかね??)
func addOne(n int) {
n = n + 1
fmt.Println("関数内:", n)
// 11
}
func main() {
x := 10
addOne(x)
fmt.Println("関数外:", x)
// 10
}
Goでは参照が渡されるのではなく、値そのものが渡されるため、値そのものを変更したい場合にはポインタを使い参照を渡します。
func addOne(n *int) {
*n = *n + 1
}
func main() {
x := 10
addOne(&x)
fmt.Println(x) // 11
}
Pythonの変数スコープに近い感覚がありますが、Pythonはオブジェクトの参照を渡していて、オブジェクトがミュータブルかイミュータブルかによって変更できるかどうかが変わってきます。
一方で、Goは値型のデータであれば、値そのものが渡されるため、オリジナルを変更するためにはポインタを渡す必要があります。ただし、スライスやマップのようなデータは参照型のデータ(内部にポインタを持っている)であるためポインタを渡す必要はありません。
このあたりはPythonのスコープを、パターンのようにして覚えていると難しく感じてしまう部分ではないかと思います。ただし、データを変更するという点でそこまで積極的に使うべきものでもないように思います。
クラス定義と構造体
データ型の中でも構造体(Struct)はGoの特徴的な仕様といえるかと思います。ここをどのようにしてとらえてあげるかが、メソッドやインタフェースの理解へとつながるのではないかと思います。
結論からいえば、私はPythonのクラス概念の対比としてGoの構造体とそれに関連する仕様を理解しました。
まずはクラス/構造体とメソッドの定義について実例をみます。
class Person:
def __init__(self, name:str, age:int):
self.name = name
self.age = age
def introduce(self) -> str:
return f"私の名前は{self.name}です。年齢は{self.age}歳です。"
if __name__ == "__main__":
p = Person("Bob", 21)
intro = p.introduce()
print(intro)
# 私の名前はBobです。年齢は21歳です。
type Person struct {
Name string
Age int
}
func (p Person) Introduce() string {
return fmt.Sprintf("私の名前は %s です。年齢は %d です", p.Name, p.Age)
}
func main() {
person := Person{
"Bob",
21,
}
intro := person.Introduce()
fmt.Println(intro)
}
クラス/構造体とメソッド定義であれば、PythonもGoも大きく変わらないように思います。Pythonがクラスを中心として内部にメソッドを定義していくのに対し、Goは構造体が中心であり、メソッドはそれに外から紐づくというイメージな感じがします。
Python経験者としてつまづくポイントはインタフェースという考え方です。ここまでクラス定義とメソッドについて似ているところがあると感じていた手前、「じゃあ次は継承とかでてくるのかな?」と思っていると、Goには継承がなく、代わりにインタフェースが存在していることがわかってきます。
インタフェースってなんなんだ?
はい、インタフェースとは一体なんなのか、どう理解すればいいのか、ここまでは結構Goわかるな~と思っていたところで急に止まります。オブジェクト指向の代表的な要素が「継承」であるとして、それとの比較で考えてみたいと思います。
# よく見る例
class Animal:
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
if __name__ == "__main__":
Dog.speak()
# Woof!
Cat.speak()
# Meow"
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }
func MakeSpeak(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
cat := Cat{}
dog := Dog{}
MakeSpeak(cat)
// Meow!
MakeSpeak(dog)
// Woof!
}
クラスについては、技術書や解説記事などで「設計図」に例えられているような気がします。Pythonでは、その設計図を広げて中を見ていくように、クラスの内部に定義された属性やメソッドを直接扱います。そして、親から子へメソッドも継承されていきます。「クラス.メソッド」という記述の通り、クラスの中からメソッドが呼び出されているような印象です。
一方、Goでは構造体がインタフェース(名前からゲートを想像してみます)を通過するときに、構造体に紐づいたメソッドがチェックされ、インタフェースの契約を満たしているかが確認されます。構造体がゲートを通る際に「メソッドがスキャンされる」ようなイメージで私は理解しました。
エラー/例外処理
最後にエラーと例外処理についてです。
Goを学び始めるまえに、簡単に特徴を調べてみると Goには例外処理がない という文言をかなり見かけたように思います。これだけでは、エラーが起きた場合にGoではどうするんだろうと思ってしまいますが、実際にはそこまで大きな違いはないように感じます。
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "ゼロでは割り算できません"
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # ゼロでは割り算できません
Pythonではtry/except/finally節を使って、エラーを処理していきます。字面からも、トライしてみる、だめならこうする、最後にはこうするという感じでとても分かりやすく直感的な感覚です。ただ、try/except節のなかがとても大きくなってしまたり、頻繁にtry/exceptがでることで可読性が落ちてしまう可能性もあります。
ここでは詳細を割愛しますが、デコレータなども利用しつつ、エラー処理もまとめるなどしていくことで、コンパクトかつわかりやすくエラーを処理できるという良さがあるように思います。
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("ゼロでは割り算できません")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
result, err = divide(10, 0)
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Println("結果:", result)
}
Pythonと大きく異なる点は、関数の実行結果から明示的にerrを受け取っている点です。実際にエラーが発生するかどうかに関わらずです。そしてその次の行でif err != nil を記述してエラーを評価しています。
Pythonのエラー処理はexcept節にジャンプしていくのに対して、Goは一連の流れの中でerrがあるかどうかを評価します。
記述量こそ多くなってしまいますが、流れのなかで処理をしていくことでシンプルさはあると感じます。
インタフェースの時に感じたような戸惑いの要素は、かなり少ないのではないかなと思います。
まとめ
基本的な仕様やデータ型を中心にPythonとの比較でGoを見てきました。
Pythonはシンプルな記述、Goは明示的な記述というように言語仕様の違いはあるものの、細かい部分については共通している部分もかなりある印象です。
また、クラス/インタフェースのように、決定的に異なっている部分についても、一方の言語との対比によって理解しやすくなる部分があるなと感じました。
さらに、ポインタや型の例のように、Goで学ぶことによって改めてPythonでの仕様について意識してみるよい機会にもなりました。
どういった流れで何をやろうとしているのかを、自分のためだけではなく他人に見てもらうことがあるような場合にも、できる限りわかりやすくありたいという気持ちがあった自分にとって、Goのような言語を学ぶことができたのはいい経験になったと感じています。
本記事ではJSONの扱いやGoの目玉でもある並列処理について触れませんでしたが、Goの実践的な利用を自分で試してみつつ、比較しながら学んでいきたいと思います。