4Go言語批評

公開: 2022-03-11

Go(別名Golang)は、人々が最も興味を持っている言語の1つです。2018年4月の時点で、TIOBEインデックスで19位になっています。 ますます多くの人々がPHP、Node.js、およびその他の言語からGoに切り替えて、本番環境で使用しています。 多くのクールなソフトウェア(Kubernetes、Docker、Heroku CLIなど)はGoを使用して作成されています。

では、成功へのGoの鍵は何ですか? 言語の中には本当にクールなものがたくさんあります。 しかし、Goを非常に人気のあるものにした主な理由の1つは、その作成者の1人であるRob Pikeが指摘したように、そのシンプルさです。

シンプルさは素晴らしいです:あなたは多くのキーワードを学ぶ必要はありません。 それは言語学習を非常に簡単かつ迅速にします。 ただし、一方で、開発者は他の言語の機能を欠いている場合があるため、回避策をコーディングするか、長期的にはより多くのコードを作成する必要があります。 残念ながら、Goには設計上多くの機能が欠けており、時にはそれが本当に煩わしいこともあります。

Golangは開発を高速化することを目的としていましたが、多くの状況では、他のプログラミング言語を使用して作成するよりも多くのコードを作成しています。 そのようなケースのいくつかを、以下のGo言語の批判で説明します。

4つの囲碁言語批判

1.関数のオーバーロードと引数のデフォルト値の欠如

ここに実際のコード例を投稿します。 GolangのSeleniumバインディングに取り組んでいたとき、3つのパラメーターを持つ関数を作成する必要がありました。 それらのうちの2つはオプションでした。 実装後は次のようになります。

 func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }

関数をオーバーロードしたり、デフォルト値を渡すことができなかったため、3つの異なる関数を実装する必要がありました。Goは設計上提供していません。 誤って間違った電話をかけたらどうなるか想像してみてください。 次に例を示します。

たくさんの`undefined`を取得します

関数のオーバーロードによってコードが乱雑になることがあることを認めなければなりません。 一方、そのため、プログラマーはより多くのコードを書く必要があります。

どうすれば改善できますか?

これがJavaScriptの同じ(まあ、ほぼ同じ)例です:

 function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }

ご覧のとおり、はるかに明確に見えます。

その上でのエリクサーのアプローチも好きです。 Elixirでの外観は次のとおりです(上記の例のように、デフォルト値を使用できることはわかっています。これは、実行可能な方法として示しているだけです)。

 defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")

2.ジェネリック医薬品の欠如

これは間違いなく、Goユーザーが最も求めている機能です。

整数の配列とそのすべての要素に適用される関数を渡すマップ関数を作成するとします。 簡単そうですね。

整数に対してそれをやってみましょう:

 package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }

よさそうだね?

さて、文字列に対してもそれを行う必要があると想像してください。 別の実装を作成する必要があります。これは、署名を除いてまったく同じです。 Golangは関数のオーバーロードをサポートしていないため、この関数には別の名前が必要になります。 その結果、異なる名前の類似した関数が多数あり、次のようになります。

 func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }

これは、DRY(Do n't Repeat Yourself)の原則に間違いなく反します。この原則では、コピー/貼り付けコードをできるだけ少なくして、代わりに関数に移動して再利用する必要があると述べています。

ジェネリックスの欠如は、何百ものバリアント関数を意味します

別のアプローチは、パラメータとしてinterface{}を使用する単一の実装を使用することですが、ランタイムタイプチェックはエラーが発生しやすいため、これによりランタイムエラーが発生する可能性があります。 また、速度が遅くなるため、これらの関数を1つとして実装する簡単な方法はありません。

どうすれば改善できますか?

ジェネリックスのサポートを含む優れた言語はたくさんあります。 たとえば、Rustの同じコードを次に示します( arrayの代わりにvecを使用して単純化しています)。

 fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }

map関数の実装は1つだけであり、カスタムのものも含め、必要なすべてのタイプに使用できることに注意してください。

3.依存関係の管理

Goの経験がある人なら誰でも、依存関係の管理は本当に難しいと言うことができます。 Goツールを使用すると、ユーザーはgo get <library repo>を実行してさまざまなライブラリをインストールできます。 ここでの問題はバージョン管理です。 ライブラリメンテナが下位互換性のない変更を加えてGitHubにアップロードすると、その後プログラムを使用しよgo getとするとエラーが発生します。これは、gogetがリポジトリをライブラリフォルダにgit cloneするだけだからです。 また、ライブラリがインストールされていない場合、そのためプログラムはコンパイルされません。

依存関係を管理するためにDepを使用することで少し改善できますが(https://github.com/golang/dep)、ここでの問題は、すべての依存関係をリポジトリに保存していることです(これは、リポジトリがコードだけでなく、数千行の依存関係コードを含めるか、パッケージリストを保存します(ただし、依存関係のメンテナが下位互換性のない変更を行うと、すべてがクラッシュします)。

どうすれば改善できますか?

ここでの完璧な例は、Node.js(そして一般的にはJavaScriptだと思います)とNPMだと思います。 NPMはパッケージリポジトリです。 さまざまなバージョンのパッケージが保存されるため、特定のバージョンのパッケージが必要な場合でも問題ありません。そこから入手できます。 また、Node.js / JavaScriptアプリケーションに含まれるものの1つは、 package.jsonファイルです。 ここでは、すべての依存関係とそのバージョンが一覧表示されているため、 npm installを使用してそれらすべてをインストールできます(そして、コードで確実に機能するバージョンを取得できます)。

また、パッケージ管理の優れた例は、RubyGems / Bundler(Rubyパッケージの場合)とCrates.io/Cargo(Rustライブラリの場合)です。

4.エラー処理

Goでのエラー処理は非常に簡単です。 Goでは、基本的に関数から複数の値を返すことができ、関数はエラーを返すことができます。 このようなもの:

 err, value := someFunction(); if err != nil { // handle it somehow }

ここで、エラーを返す3つのアクションを実行する関数を作成する必要があると想像してください。 次のようになります。

 func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }

ここには繰り返し可能なコードがたくさんありますが、これは良くありません。 そして、大きな関数では、それはさらに悪化する可能性があります! これには、おそらくキーボードのキーが必要になります。

キーボードのエラー処理コードのユーモラスな画像

どうすれば改善できますか?

そのためのJavaScriptのアプローチが好きです。 関数はエラーをスローする可能性があり、それをキャッチすることができます。 例を考えてみましょう。

 function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }

それははるかに明確であり、エラー処理のための繰り返し可能なコードが含まれていません。

Goの良いこと

Goには設計上多くの欠陥がありますが、いくつかの非常に優れた機能もあります。

1.ゴルーチン

非同期プログラミングはGoで本当にシンプルになりました。 マルチスレッドプログラミングは通常、他の言語では困難ですが、新しいスレッドを生成し、その中で関数を実行して、現在のスレッドをブロックしないようにするのは非常に簡単です。

 func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }

2.Goにバンドルされているツール

他のプログラミング言語では、さまざまなタスク(テスト、静的コードのフォーマットなど)に対してさまざまなライブラリ/ツールをインストールする必要がありますが、デフォルトでGoにすでに含まれているクールなツールがたくさんあります。

  • gofmt静的コード分析のためのツール。 eslintjshintなどの追加の依存関係をインストールする必要があるJavaScriptと比較すると、ここではデフォルトで含まれています。 また、Goスタイルのコードを記述しない場合(宣言された変数を使用しない、未使用のパッケージをインポートするなど)、プログラムはコンパイルされません。
  • go testテストフレームワーク。 繰り返しになりますが、JavaScriptと比較すると、テスト用に追加の依存関係(Jest、Mocha、AVAなど)をインストールする必要があります。 ここでは、デフォルトで含まれています。 また、ベンチマーク、ドキュメント内のコードのテストへの変換など、デフォルトで多くの優れた処理を実行できます。
  • godocドキュメントツール。 デフォルトのツールに含まれていると便利です。
  • コンパイラ自体。 他のコンパイル言語と比較して、信じられないほど高速です。

3.延期する

これは、この言語で最も優れた機能の1つだと思います。 3つのファイルを開く関数を作成する必要があると想像してください。 また、何かが失敗した場合は、開いている既存のファイルを閉じる必要があります。 そのような構造がたくさんあると、それは混乱のように見えます。 この擬似コードの例を考えてみましょう。

 function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }

複雑に見えます。 そこで、Goのdeferが行われます。

 package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }

ご覧のとおり、ファイル番号3を開くときにエラーが発生した場合、他のファイルは自動的に閉じられます。これは、 deferステートメントが逆の順序で返される前に実行されるためです。 また、関数の異なる部分ではなく、同じ場所でファイルを開いたり閉じたりするのも便利です。

結論

Goの良い点と悪い点のすべてについては触れませんでしたが、私が最も良い点と悪い点についてだけ言及しました。

Goは、現在使用されている興味深いプログラミング言語の1つであり、実際に可能性を秘めています。 それは私たちに本当にクールなツールと機能を提供します。 ただし、そこで改善できることがたくさんあります。

Go開発者としてこれらの変更を実装すると、Goを使用したプログラミングがはるかに快適になるため、コミュニティに大きなメリットがあります。

それまでの間、Goを使用してテストを改善しようとしている場合は、 Goアプリのテストを試してください。ToptalerGabrielAssalosの仲間による正しい方法で始めましょう。

関連:適切に構造化されたロジック:GolangOOPチュートリアル