1. ホーム
  2. スクリプト・コラム
  3. ゴラン

Golangを他の言語と区別する9つの特徴

2022-02-15 08:30:12

プログラミング言語が進化していく中で、Goはまだ非常に若い言語です。最初にリリースされたのは、2009年11月10日です。開発者のロバート・グリーセマー(Robert Griesemer)は
Rob Pike と Ken Thompson は Google に勤務していましたが、大規模なスケーリングという課題から、複数の開発者によって管理され、厳しいパフォーマンスが要求され、複数のネットワークと処理コアをまたぐ大規模なコードベースを持つための高速で効率的なプログラミング ソリューションとして Go を設計することになりました。
また、Goの創設者たちは、新しい言語を作るにあたって、他のプログラミング言語の長所、短所、脆弱性を学ぶ機会を持ちました。その結果、比較的少数のコマンドと関数で構成される、すっきりとして明快で実用的な言語が生まれました。

本日の記事では、Goが他の言語と異なる9つの特徴についてご紹介します。

1. Goは常にビルドにバイナリを含む

Go ランタイムは、メモリ割り当て、ガベージ コレクション、並行処理サポート、ネットワーキングなどのサービスを提供します。ランタイムは各 Go バイナリにコンパイルされています。他の多くの言語では、正しく動作させるためにプログラムと共にインストールする必要がある仮想マシンを使用します。

ランタイムをバイナリに直接含めることで、Go プログラムの配布と実行が容易になり、ランタイムとプログラム間の非互換性の問題も回避できます。Python、Ruby、JavaScript などの言語の仮想マシンもガベージコレクションとメモリ割り当てに最適化されていないので、他の類似言語と比べて Go が優れた速度を持つ理由がわかります。たとえば、Goはできるだけ多くのストレージをスタックに置き、ヒープよりも高速にアクセスできるようにデータの順序を決めています。これについては後で詳しく説明します。

Goの静的バイナリの最後の特徴は、実行するための外部依存がないため、非常に高速に起動することです。これは、Google Cloud 上で動作する platform-as-a-service である Google App Engine のようなサービスを使用している場合に便利で、アプリをゼロ インスタンスに縮小してクラウド コストを節約することが可能です。App Engineは、新しいリクエストを受けると、瞬く間にGoプログラムのインスタンスを立ち上げることができます。PythonやNodeで同じ体験をすると、必要な仮想環境も新しいインスタンスと一緒に起動するため、通常3~5秒(またはそれ以上)の待ち時間が発生します。

2. Go には、プログラムの依存関係を一元的に管理するサービスがない

公開された Go プログラムにアクセスするために、開発者は、Java の Maven Central や JavaScript の NPM レジストリのような、一元的にホストされるサービスには依存しません。代わりに、プロジェクトはソース コード リポジトリ (最も一般的なのは Github) を通して共有されます。go installコマンドラインは、この方法でライブラリをダウンロードすることを可能にします。
なぜこの機能が好きなのか?Maven Central、PIP、NPMのような中央でホストされる依存性サービスは、依存性のダウンロードとインストールの手間を省いてくれるかもしれませんが、依存性が間違っているときに恐ろしいハートビート停止を引き起こす必然的なブラックボックスだといつも感じています。

さらに、モジュールを他の人が利用できるようにするには、バージョン管理システムに入れるだけでよく、プログラムを配布するのに非常に簡単な方法です。

3. Goは値で呼び出す

Go では、関数の引数に生の値(数値、ブーリアン、文字列)または構造体(クラスオブジェクトに相当する大まかなもの)を与えると、Go は常にその変数の値をコピーします。

Java、Python、JavaScriptなど他の多くの言語では、元の言語は値で渡されるが、オブジェクト(クラスインスタンス)は参照で渡される。これは、受け取った関数が実際には元のオブジェクトのコピーではなく、そのポインタを受け取ることを意味する。受信関数でオブジェクトに加えられた変更は、元のオブジェクトに反映される。

Goでは、構造体とプリミティブはデフォルトで値で渡されますが、アスタリスク演算子を使ってポインタを渡すオプションもあります。

// Passing by value
func MakeNewFoo(f Foo ) (Foo, error) { 
   f.Field1 = "New val" 
   f.Field2 = f.Field2 + 1 
   return f, nil 
}


上記の関数は Foo のコピーを取り、新しい Foo オブジェクトを返します。

// passed by reference
func MutateFoo(f *Foo ) error { 
   f.Field1 = "New val" 
   f.Field2 = 2 
   return nil 
}


上記の関数は、Fooへのポインタを受け取り、元のオブジェクトを変更します。

値による呼び出しと参照による呼び出しのこの明らかな違いは、あなたの意図を明らかにし、呼び出し関数が(多くの初心者開発者が苦労している)起こるはずのないときに誤って入力オブジェクトを変更する可能性を低くしますホールドタイト)。

MITが要約しているように、「ミュータビリティは、プログラムが何をしているかを理解するのを難しくし、契約を強制するのを難しくする」のです"

さらに、コール・バイ・バリューはガベージコレクタの作業を大幅に軽減するため、アプリケーションの高速化とメモリ効率の向上を実現します。この記事では、ポインタトレース(ヒープからポインタの値を取り出すこと)は、連続したスタックから値を取り出すことに比べて10~20倍遅くなると結論づけています。メモリから最も速く読み出す方法は順次読み出すことであり、これは RAM にランダムに格納されるポインタの数を最小にすることである、という経験則を覚えておくとよいでしょう。

4. 'defer' キーワード

NodeJSでは、knex.jsを使い始める前は、必要なデータベースのCRUD関数が完了したら、データベースプールを作成し、関数ごとにプールから新しい接続をオープンして、コード内のデータベース接続を手動で管理していました。

なぜなら、各関数の最後で接続を解放しないと、解放されていないデータベース接続の数が徐々に増えていき、プールから利用可能な接続がなくなって、アプリケーションが壊れてしまうからです。

現実には、プログラムはしばしばリソース、ファイル、接続などを解放、クリーンアップ、削除する必要があるため、これらを効率的に管理する方法として、Goはdeferキーワードを導入しました。

deferで始まるステートメントは、その周りの関数が終了するまで、その呼び出しを遅らせます。つまり、関数の先頭に後始末や解体用のコードを置いても、関数が終了すればそのコードが実行されることが分かっているのです(当たり前ですが)。

func main() {                        
    if len(os.Args) < 2 {   
        log.Fatal("no file specified")
    }  
    f, err := os.Open(os.Args[1])                        
    if err ! = nil {                         
        log.Fatal(err)                        
    }                        
    defer f.Close()                        
    data := make([]byte, 2048)                        
    for {                         
        count, err := f.Read(data)                                               
        os.Stdout.Write(data[:count])                        
        if err ! = nil {                          
            if err ! = io.EOF {                           
                log.Fatal(err)                          
            }                          
            break                         
        }                        
    }                       
}


上の例では、ファイルクローズ・メソッドを遅延させています。私はこのパターンが好きです。関数の先頭でハウスキーピングの意図を宣言し、関数が終了したらその仕事を終えることを知っているので、そのことを忘れてしまうのです。

5. Goは関数型プログラミングの最良の特徴を採用している

関数型プログラミングは効率的で創造的なパラダイムです。幸いなことに、Goは関数型プログラミングの最良の特徴を採用しています。Goでは

  • 関数は値です。つまり、値としてマッピングに追加したり、他の関数に引数として渡したり、変数として設定したり、関数から返したりできます("高次関数"と呼び、Goではしばしばデコレーターを使ってミドルウェアのパターンを作成するために使用されます)。
  • 匿名関数を自動的に作成し、呼び出すことができる。
  • 他の関数の内部で宣言された関数はクロージャが可能です(関数の内部で宣言された関数は、外部関数で宣言された変数にアクセスし、変更することができます)。慣用的な Go では、クロージャは関数のスコープを制限したり、関数がロジックで使用する状態を設定するために広く使用されています。
func StartTimer (name string) func(){
    t := time.Now()
    log.Println(name, "started")
    return func() {
        d := time.Now().Sub(t)
        log.Println(name, "took", d)
    }
}
func RunTimer() {
    stop := StartTimer("My timer")
    defer stop()
    time.Sleep(1 * time.Second)
}


上記はクロージャの一例です。StartTimer' 関数は、クロージャを介してその誕生範囲に設定された 't' 値にアクセスできる新しい関数を返します。この関数は、現在の時刻と 't' の値を比較して、便利なタイマーを作成することができます。この例を提供してくれたMat Ryerに感謝します。

6. 囲碁には暗黙のインターフェイスがある

SOLIDコーディングやデザインパターンに関する文献を読んだことがある人なら、「継承よりも組み合わせを優先する」というマントラを聞いたことがあるのではないでしょうか。つまり、親クラスからのプロパティやロジックの階層的な継承に頼るのではなく、ビジネスロジックを個別のインターフェースに分解すべきだということです。

APIは期待される動作のコントラクト(メソッドの署名)のみを公開し、その動作をどのように実装するかの詳細については公開しない、というものです。

いずれも、現代のプログラミングにおけるインターフェースの重要性を示しています。

したがって、Goがインターフェースをサポートしていることは、驚くことではありません。実際、インターフェイスはGoで唯一の抽象型です。

しかし、他の言語とは異なり、Goのインターフェースは明示的に実装されるのではなく、暗黙的に実装されます。具象型はインターフェイスを実装していることを宣言しない。その代わり、その具象型に設定されたメソッドセットが基礎となるインターフェースのすべてのメソッドセットを含んでいれば、Goはそのオブジェクトがインターフェースを実装していると見なします。

このインターフェースの暗黙の実装(正式には構造型と呼ばれる)により、Goは型安全性とデカップリングを強制し、動的言語で示される柔軟性の多くを維持することができるのです。

これに対して、明示的なインターフェースはクライアントと実装を結びつけるため、例えばJavaでは依存関係を置き換えることがGoよりもずっと難しくなります。

// This is an interface declaration (called Logic)
type Logic interface { 
    Process (data string) string 
string }

type LogicProvider struct {}
// This is the method named "Process" on LogicProvider struct
 func (lp LogicProvider) Process (data string) string { 
    // Business logic
}
// This is the client structure with the Logic interface as an attribute
type Client struct { 
    L Logic 
}
func(c Client) Program() { 
    // Get data from somewhere
    cLProcess(data) 
}
func main() { 
    c := Client { 
        L: LogicProvider{},  
     } 
    c.Program() 
}


LogicProviderには、Logicインターフェースに準拠する旨の宣言はない。これは、クライアントが将来、そのLogicProviderを簡単に交換できることを意味し、そのLogicProviderが基礎となるインターフェース(Logic)のすべてのメソッド・セットを含む限り、LogicProviderを交換できる。

7. エラー処理

Goのエラー処理は、他の言語とは大きく異なります。簡単に言うと、Goは関数の最後の戻り値としてerror型の値を返すことでエラーを処理します。

error 引数は、関数が期待通りに動作した場合は nil を返し、そうでない場合はエラー値を返します。関数を呼び出すと、エラーの返り値をチェックしてエラーを処理するか、自分自身のエラーを投げます。

// The function returns an integer and an error
func calculateRemainder(numerator int, denominator int) ( int, error ) { 
   //
   if denominator == 0 { 
      return 9, errors.New("denominator is 0"
    }
   // no error returned
   return numerator / denominator, nil
 }


Goがそのように機能するのには理由があります。コーダーに例外を考慮し、適切に処理することを強いるのです。従来の try-catch 例外は、コードに少なくとも1つの新しいコードパスを追加し、それを追いかけるのが困難な方法でインデントします。

8. 並行処理

おそらく Go の最も有名な機能である並行処理では、マシンやサーバーで利用可能なコアの数だけプロセスを並行して実行することができます。同時実行が最も意味を持つのは、別々のプロセスが互いに依存せず(順次実行する必要がない)、時間的なパフォーマンスが重要な場合です。これは、ディスクやネットワークへの読み書きは、最も複雑なインメモリプロセスを除いて、数桁も遅いというI/O要件によく当てはまります。
関数呼び出しの前に "go" キーワードを付けると、その関数が同時に実行されます。

func process(val int) int { 
   // Do something with val
}
// For each value in 'in', run the process function simultaneously
// and read the result of the process to 'out'
 func runConcurrently(in <-chan int, out chan<- int){ 
   go func() { 
       for val := range in { 
            result := process(val) 
            out <- result   
       } 
   } 
}


Goの並行処理は奥が深く、かなり高度な機能ですが、意味がある場合には、プログラムの最適なパフォーマンスを確保するための効率的な方法を提供します。

9. Go標準ライブラリ

Goはquot;inclusive battery"の哲学を持ち、現代のプログラミング言語の要求の多くが標準ライブラリに組み込まれ、プログラマーの生活をより快適なものにしています。
前述したように、Goは比較的若い言語であるため、現代のアプリケーションの問題点・要求の多くを標準ライブラリで満たすことができるのです。

一方で、Goはウェブ(特にHTTP/2)とファイル管理に対して世界トップクラスのサポートを提供しています。また、ネイティブのJSONエンコードとデコードも提供されています。そのため、HTTP リクエストを処理して応答 (JSON またはその他) を返すサーバーのセットアップは非常に簡単で、REST ベースの HTTP Web サービス開発で Go が人気を博している理由がわかります。

Mat Ryerも指摘しているように、標準ライブラリはオープンソースであり、Goのベストプラクティスを学ぶのに最適な方法です。

この記事から本当に何か新しいことを学んだのなら、それを好きになって、ブックマークして、あなたの仲間と共有してください。???? 最後に、❤または? 応援よろしくお願いします。

Golangが他の言語と異なる9つの特徴についてのこの記事はこれで終わりです。Golangの機能についてより詳しく知りたい方は、Scripting Houseの過去の記事を検索するか、以下の関連記事を引き続きご覧ください。