■はじめに
この記事ではPHPerがGo言語に入門してみて感じた、PHPとの違いなどを簡単にまとめています。
現PHPerの方がGo言語入門する際になんとなくでも雰囲気を掴んで頂けたら幸いです。
筆者は25年1月時点でGo言語には約2ヶ月しか触れていないので、間違っている箇所や分かりづらい点等あればご指摘頂けますと幸いです。
■そもそもGo言語はどのような言語?
Go言語は元々Google社内の課題解決のために開発されたプログラミング言語です。
◎開発スケール
- 開発の効率化と簡素化
- 高速なビルド処理
├ 効率的な依存関係の解決
├ ビルド速度を妨げる要因の排除 - 実装のしやすさを重視
◎製品のスケール
- 大規模システムの安定運用に対応
- マルチコアプロセッサの活用
これらのような課題を解決するためにGo言語が開発されました。
Go言語は静的型付け言語であり、PHPのようなインタプリタ型言語とは異なり、コンパイラを必要とします。
PHPはインタプリタ型言語のため、リソースを多く消費する処理の実行に時間がかかることがある一方で、Go言語はGoroutinesとChannelによる並行処理をサポートしており、複数のタスクを同時に処理できるため、PHPよりも高いパフォーマンスを実現することができます。
Go言語とPHPを比較するとそれぞれにメリット・デメリットがありますが、ここでは筆者が特にPHPとは明確に違うなと感じた箇所部分をまとめていきます。
■Go言語を触ってみて感じた違い
1. 言語仕様
ここでは3つの違いについてまとめます。
組み込み関数の数
▼ポイント
- Go言語は組み込み関数が少ない
- その分豊富な標準パッケージからインポートして利用することができる
- Go言語はシンプルかつ一貫性を重視した思想
まず第一に感じたのは、PHPにはあったのにGo言語にはない….といった組み込み関数が多いということです。
この理由は、Go言語とPHPの設計思想に違いがあるためです。
-
Go言語
Goは、標準ライブラリをシンプルかつ効率的に設計しています。
標準ライブラリは充実しているものの、組み込み関数の数は最小限に抑えられており、必要な機能は標準パッケージをインポートして利用するのが基本です。
-
PHP
PHPはWeb開発向けに設計された言語であり、組み込み関数の数が非常に多いことが特徴の1つとして挙げられます。
標準ライブラリをインポートしなくても、文字列操作・配列処理・ファイル操作・データベースアクセスなど、多くの機能をすぐに利用できます。
しかし、例えばファイル操作などは同じような処理をできるメソッド(
file_get_contents
やfopen
など)が複数あったりするので、人によって利用するメソッドがマチマチになりがちなのはあるあるではないでしょうか。
Go言語では、地味に使う機会の多いファイル操作(osパッケージ)やDb操作(databaseパッケージ)などを行うメソッド等が標準パッケージに組み込まれており、組み込み関数としては用意されていません。
一方でPHPはこれらの操作をインポートなど考えずにいつでもどこでも利用できるため、利用ハードルがとても低いです。
とはいえ、Go言語ではパッケージをインポートするという行為を挟むことで依存関係のわかりやすさ、そして豊富な標準パッケージから機能を利用することで、コードの統一感と一貫性を担保でき、PHPでは利便性を重視していることがわかります。
少し話は反れますが、統一感・一貫性という意味ではパッケージの命名規則にも大きな違いがあります。
-
Go
- 一貫した命名規則(
camelCase
が基本)。 - 標準パッケージに整理されており、例えば文字列操作なら
strings
パッケージ、数学関数ならmath
パッケージを利用する。
fmt.Println(len("hello")) // 長さを取得 fmt.Println(strings.ToUpper("hello")) // 文字列を大文字に変換
- 一貫した命名規則(
-
PHP
-
一貫性のない命名規則(
snake_case
やcamelCase
、str_*
のようなプレフィックスが混在)。- 特に
composer
などで外部ライブラリをインストールしたりすると明白です
- 特に
-
グローバルスコープに大量の組み込み関数が存在する。
echo strlen("hello"); // 長さを取得 echo strtoupper("hello"); // 文字列を大文字に変換
-
PHPでは標準的なコーディング規約がPSRとして公開されていますが、基本的にはプロジェクトで策定するのが一般的かと思います。
しかし、コーディング規約を遵守していなければ時間が経つにつれプロジェクトメンバーの入れ替わりなどを経て、色んな人によって様々な書き方が発生し、見づらいコードとなっていくのは利便性を重視したPHPのデメリットと言えるかもしれません。
配列が固定長
▼ポイント
- 配列は固定長
- 可変長を扱うにはスライスもしくはマップを利用
Go言語のコードリーディングをする中で一番戸惑ったのは、配列が固定長であるということです。
PHPでは配列に対して値を追加したり減らしたりしながら処理を行うことも頻繁にあったのに対し、Go言語では配列の要素数を変更することができません。
以下はGo言語で配列を操作してみた例です。
Go言語における配列は同じ型の集まりで、連結したメモリ位置に格納されます。
func main() {
var a [2]int // これで int型 の 要素が2つ の配列を作るという定義になる.
a[0] = 100
a[1] = 200
var b = [2]int{100, 200} // {}を利用しているが、オブジェクトではないので惑わされないように
// b = append(b, 300) // 配列はサイズを変更することができない
fmt.Println(a)
fmt.Println(b)
}
// 結果
[100 200]
[100 200]
append
で配列のサイズを変更しようとすると以下のようなエラーが表示されます。
first argument to append must be a slice; have b (variable of type [2]int)
(翻訳)append
の最初の引数はスライスでなければなりませんが、b
(型が [2]int
の変数)が指定されています。
この記事では記載しませんが、Go言語において配列の要素数などを変更する場合は、「スライス」を利用することで可変長として利用することができるようになります。
var c []int = []int{100, 200}
c = append(c, 300) // スライスは要素数が可変長な配列なので、 append でもエラーがでない
fmt.Println(c)
また、連想配列を定義する場合は、ブラケットを2つ続けることで定義することができます。
// 外側が 2、要素の中身が 3 であるint型の連想配列の定義
var boardArray = [2][3]int{
[3]int{0, 1, 2},
[3]int{3, 4}, // 足りない要素はその型のゼロ値 = 初期値が挿入される
}
fmt.Println(boardArray)
// 結果
[[0 1 2] [3 4 0]]
// 外側が 2、要素の中身が 2 であるstring型の連想配列の定義
boardStringArray := [2][2]string{
[2]string{"りんご", "ばなな"},
[2]string{"いちご"},
}
fmt.Println(boardStringArray)
// 結果
[[りんご ばなな] [いちご ]]
/** ちなみに、連想配列の中身では型を指定する必要がない(IDEによって指摘される) */
var boardArray = [2][3]int{
{0, 1, 2},
{3, 4},
}
fmt.Println(boardArray)
boardStringArray := [2][2]string{
{"りんご", "ばなな"},
{"いちご"},
}
fmt.Println(boardStringArray)
最後の配列には要素が1つずつ足りませんが、結果を見るとint型の場合はゼロ値である0
が、string型の場合はゼロ値である空文字(""
)が挿入されます。
このように、配列に対してどのような操作を行うかで対応が変わってくる点はPHPとは違う観点だといえます。
ポインタ型
やはりPHPとの言語の違いで一番の目玉は、Go言語ではポインタ操作を行う必要があるという点ではないでしょうか。
このポインタの扱いに現在進行形で苦戦しているのですが、弊社先輩の良記事を参考にさせて頂き絶賛勉強中です
ここが1つの山といっても過言ではないかもしれません
ポインタについては是非以下の記事をご覧ください🤲
2. 文法
文法はGo言語によるショートハンドが提供されてはいるものの、基本的にPHPと似たような記述の仕方をすることができます。
ただ、その中でも書いていてこれは違うなと思った「for文」と「switch文」の2つについてまとめます。
for文
▼ポイント
-
foreach
がない-
foreach
と同様の処理を行うにはrange
キーワードが必要
-
for文は以下のように他の言語でも見るような書き方をすることができます。
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
// 結果
0
1
2
3
4
5
6
7
8
9
continue
やbreak
も利用することができます。
func main() {
for i := 0; i < 10; i++ {
if i == 3 {
fmt.Println("continue")
continue
}
if i > 5 {
fmt.Println("break")
break
}
fmt.Println(i)
}
}
// 結果
1
2
continue
4
5
break
ただ、PHPerであればfor文は特別な理由がない限りほぼ使わず、foreach
で済ませていたという方もいらっしゃるのではないでしょうか。
僕もその一人でした。
結論から言うとGo言語にforeach
という文法は存在しません。
Go言語ではrange
キーワードとfor文を組み合わせて再現することになります。
例えば配列をループしたい場合は以下のように書くことでforeach
と同様の処理を実現することができます。
func main() {
ary := [3]string{"php", "go", "typescript"}
for k, v := range ary {
fmt.Println(k, v)
}
}
// 結果
0 php
1 go
2 typescript
上記は配列を例に上げましたが、同様にスライス・マップも同様に処理することができます。
func main() {
s := []string{"php", "java", "ruby"}
m := map[string] int {
"javascript": 1999,
"typescript": 2010,
}
for sk, sv := range s {
fmt.Println(sk, sv)
}
for mk, mv := range m {
fmt.Println(mk, mv)
}
}
// 結果
0 php
1 java
2 ruby
javascript 1999
typescript 2010
余談ですが、値の利用の仕方も非常に簡単であり、
- インデックスのみを利用する場合
- インデックスを利用せず、値のみを利用する場合
の2パターンについても非常に簡潔に表現することができます。
func main() {
m := map[string]int {
"apple": 100,
"banana": 200,
"grape": 300,
}
// インデックスのみを利用する場合.
for i := range m {
fmt.Println(i)
}
// インデックスを利用せず、値のみを利用する場合.
for _, v := range m {
fmt.Println(v)
}
// [おさらい]両方利用する場合
for k, v := range m {
fmt.Println(k, v)
}
}
// 結果
apple
banana
grape
100
200
300
apple 100
banana 200
grape 300
なお、インデックスを利用しない場合、for文内で_
を利用することはできない点には注意が必要です。
func main() {
s := []int{1, 2, 3}
for _, v := range s {
fmt.Println(_, v) // cannot use _ as value or type
}
}
foreach
という文法が存在しないのも、Go言語の設計思想と一貫性を鑑みた結果だと考えられます。
for文を用いてforeach
と同じ結果をもたらすことができる方法があれば、for文1つで事足りるため余計なことを覚える必要がなく、最小限の構文で最大限の柔軟性が担保できるようになります。
switch文
▼ポイント
- breakを書かなくてよい
-
switch
-case
を1タブ開かずに同列で記述できる - ショートハンドが利用できる
Go言語で書くswitch文はPHPと比べてかなり削ぎ落とされて非常に簡潔に書ける構文となっています。
func main() {
os := "mac"
switch os {
case "mac"
fmt.Println("This is Mac OS")
case "windows"
fmt.Println("This is Windows OS")
case "linux"
fmt.Println("This is Linux OS")
default
fmt.Println("This is Unknown OS")
}
}
// 結果
This is Mac OS
まずPHPやJavaScriptのswitch文との大きな違いは、各case
にbreak
を書く必要がない点です。
また、switch
- case
を1タブ開かずに同列で記述するのも地味に違う点かもしれません。
なお、if文などと同様に省略記法も利用することができます。
func main() {
switch os := getOsName(); os {
case "mac"
fmt.Println("This is Mac OS")
case "windows"
fmt.Println("This is Windows OS")
case "linux"
fmt.Println("This is Linux OS")
default
fmt.Println("This is Unknown OS", os) // 変数osはスコープ内.
}
// これはスコープ外になるので要注意.
fmt.Println(os)
}
func getOsName() string {
return "mac"
}
// 結果
This is Mac OS
また、参考程度にswitch
- true
イディオムも利用することはできますので載せておきます。
※一般的にswitch
- true
イディオムはアンチパターンと言われることがありますので利用には注意してください。
func main() {
t := time.Now()
fmt.Println(t.Hour()) // 19
switch {
case t.Hour() < 12
fmt.Println("Good Morning")
case t.Hour() < 17
fmt.Println("Good Evening")
case t.Houre() < 20
fmt.Println("Good Night")
}
}
// 結果
Good Night
3. 自動フォーマット
PHPには公式的にフォーマッターが存在しません。
必要であればサードパーティー製ツールをcomposer install
する必要がありますが、統一されたフォーマットを提供してくれるわけではありません。
Go言語にはコード全体のスタイルを統一する自動フォーマットツールgofmt
が標準で備わっています。
gofmt
はGoの標準ツールの1つで、コードを自動的に整形してくれます。
Goの公式スタイルガイドに準拠し、コードの一貫性を維持するように設計されているのが特徴です。
たとえば、以下のような不適切なインデントのコードがあるとします
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
gofmt
を実行すると、以下のように自動的に整形してくれます。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
この機能により、コーディングスタイルに関する議論が不要となり、開発者はロジックの実装に集中することができるようになります。
自動フォーマットしてくれることで誰が書いても同じフォーマットのコードが生成されるだけでなく、PRレビュー時にインデントなどを気にする必要がないのはレビュワーとしても非常に気が楽だなと感じました。
■最後に
この記事ではPHPerがGo言語に入門してみて感じた両者の違いなどを簡単にご紹介しました。
まだまだ簡単な部分の違いしかわかっていませんが、今後更に勉強して、更に深い部分の違いなどについてまとめられたらと思います。