Go言語:命令型プログラミングのリハビリテーション

ほぼすべての最新のプログラミング言語には、何らかの形でオブジェクト指向機能が含まれていますが、Goの作成者は、可能な限り命令型パラダイムに制限しようとしています。 言語の作者の1人がケントンプソン (UNIXおよびCの開発者)であることを考えると、これは驚くべきことではありません。 このような言語の明確な命令は、経験豊富なオブジェクト指向プログラマーを当惑させ、そのような言語で現代の問題を解決する可能性について疑念を抱かせる可能性があります。



この記事の目的は、Goに興味のあるプログラマーが言語の必須機能を理解できるようにすることです。 特に、主要な設計パターンの実装を支援します。 さらに、Go自体、その標準ライブラリとツールに実装された興味深いソリューションがあり、多くの人を喜ばせます。



はじめに:タイプ、構造、変数



多くの命令型プログラミング言語(C / Algol / Pascalなど)と同様に、重要なエンティティは構造です。 Goでは、構造は次のように定義されます。



type User struct{ Name string Email string Age int }
      
      





構造に加えて、エイリアスも同じ方法で宣言できます。



 type UserAlias User type Number int type UserName string
      
      





構造体のインスタンスを含む変数を作成するには、いくつかの方法があります。



 //     var user0 User //       user1 := User{} //    user2 := make(User, 1) user3 := &User{} //         nil var user4 *User
      
      





初期化中の構造体フィールドの名前は、宣言シーケンスを維持しながら省略できます。



 u1 := User{Name: "Jhon", Email: "jhon@example.or", Age: 27} u2 := User{"Jhon", "jhon@example.or", 27}
      
      





なぜなら Goにはガベージコレクターが組み込まれているため、直接インスタンス化される変数とリンクを介してインスタンス化される変数に違いはありません。

スコープからリンクを終了してもメモリリークは発生せず、少なくとも1つのリンクが存在する場合、値によってインスタンス化された値は解放されません。 範囲外。

つまり 次のコードは完全に安全ですが、C / C ++の同様の構造は致命的な結果につながる可能性があります。



 type Planet struct{ Name string } func GetThirdPlanetByRef() *Planet{ var planet Planet planet.Name = "Earth" return &planet } func GetThirdPlanetByVal() Planet{ var planet *Planet planet = &Planet{Name: "Earth"} return *planet }
      
      







継承ではなくインターフェースと匿名フィールド



Goには習慣的な継承はありませんが、継承を送信メカニズムとして考える場合、a)特定のタイプに属する、b)特定の動作の送信、c)基本フィールドの送信、匿名フィールドとインターフェイスはそのような継承メカニズムに起因する可能性があります。



匿名フィールドは、構造内の重複したフィールド記述を避けます。 したがって、たとえば、ユーザー構造があり、この構造に基づいて、バイヤーバイヤーとキャッシャーキャッシャーをさらにいくつか作成する必要がある場合、新しい構造のフィールドは次のようにユーザーから借用できます。



 type Buyer struct { User Balance float64 Address string } type Cashier struct { User InsurenceNumber string }
      
      





ユーザーは「家族のつながり」でつながっておらず、バイヤーがユーザーの子孫であるとは言いませんが、ユーザー構造のフィールドはバイヤー/キャッシャーで利用できます。



一方、User / Buyer / Cashierのメソッドを個別に実装する必要がありますが、あまり便利ではありません。 巨大な複製につながります。

代わりに、同じ動作を実装するメソッドを、共通のインターフェイスを引数として取る関数に変換できます。 例は、SendMailにメッセージを送信する方法(テキスト文字列)です。 なぜなら 各構造に必要なのは電子メールだけで、GetEmailメソッドの要件を備えたインターフェイスを作成すれば十分です。



 type UserWithEmail interface { GetEmail() string } func SendMail(u *UserWithEmail, text string) { email := u.GetEmail() //    email } func main() { //  users      users := []UserWithMail{User{}, Buyer{}, Cashier{}} for _, u := range users { SendEmail(u, "Hello world!!!") } }
      
      







カプセル化



Goにはアクセス修飾子がありません。 変数、構造、または関数の可用性は、識別子に依存します。

Goは、識別子が両方の条件を満たすエンティティのみをエクスポートします。



  1. 識別子は大文字で始まります(Unicodeクラス「Lu」)
  2. 識別子はパッケージブロックで宣言されている(つまり、どこにもネストされていない)か、メソッドまたはフィールドの名前です


つまり、識別子を非表示にするには、小さな文字で名前を付けます。



タイプディスパッチ



本質的に、Goにはアドホックなポリモーフィズムがなく、パラメトリックなポリモーフィズム(つまり、Javaジェネリックとc ++テンプレート)がなく、サブタイプの明示的なポリモーフィズムはありません。

つまり、同じモジュールで同じ名前と異なるシグネチャを持つ2つの関数を定義することはできません。また、異なるタイプに共通のメソッドを作成することもできません。

つまり Goの次の構成体はすべて違法であり、コンパイルエラーが発生します。



 func Foo(value int64) { } //   "Foo redeclared in this block", ..    func Foo(value float64) { } type Base interface{ Method() } //   "invalid receiver type Base (Base is an interface type)", ..      func (b *Base) Method() { }
      
      





ただし、Goには、多態的な動作をエミュレートする2つのメカニズムがあります。

これは、第一に、動的なタイプのディスパッチであり、第二に、アヒルのタイピングです。



したがって、Goのオブジェクトは、インターフェイス{}型に縮小できます。これにより、任意の型の変数を関数に渡すことができます。



 package main func Foo(v interface{}) { } func main() { Foo(123) Foo("abs") }
      
      





なぜなら インターフェイス{}に独自のメソッドを含めることはできません。型へのアクセスを返すために、特別なスイッチ型の構造があります。



 func Foo(v interface{}) { switch t := v.(type) { case int: //   t   int case string: //   t   string default: //   } }
      
      







可変寿命管理



Goにはコンストラクタまたはデストラクタがありません。 複雑な構造のインスタンスを作成するために、Newで始まる特別な関数が定義されています。例:



 func NewUser(name, email string, age int) *User { return &User{name, email, age} }
      
      





このようなコンストラクター関数の存在は、構造を直接インスタンス化する機能を制限しません。 ただし、このアプローチは標準のGoライブラリでも使用されており、大規模なアプリケーションでコードを体系化するのに役立ちます。



Goでデストラクタを使用する状況はさらに複雑です。 C ++で使用可能な機能と同様の機能を完全に実装することはできません。



リソースを解放する必要がある場合は、Releaseメソッドを作成できます。



 func (r *Resource) Release() { // release resources }
      
      







もちろん、C ++で発生するように、変数がスコープ外に出た場合や例外が発生した場合、このメソッドは単独で呼び出されません(Goには例外はありません)。 そのような状況では、 延期、パニック、および回復メカニズムを使用することをお勧めします。 たとえば、Deferディレクティブを使用してReleaseメソッドを遅延させることができます。



 func Foo() { r := NewResource() defer r.Release() if err := r.DoSomething1(); err != nil { return } if err := r.DoSomething2(); err != nil { return } if err := r.DoSomething3(); err != nil { return } }
      
      





これにより、シナリオに関係なく、Foo関数を呼び出した後にリソースを解放できます。

遅延動作は常に予測可能であり、3つのルールで記述されます。



  1. 遅延関数の引数は、遅延コンストラクトが形成されるときに計算されます。
  2. 遅延関数は、フレーミング関数のメッセージを返した後、「最後に入力された-最初の左」の順序で呼び出されます。
  3. 遅延関数は、名前付き戻り値を読み取って変更できます。


組み込みのパニックおよび回復機能は、例外の代わりとして機能します。



 func Bar() { panic("something is wrong") } func Foo() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in Bar: ", r) } }() Bar() fmt.Prinln("this message will not be printed on panic inside Bar") }
      
      





パニックにより、すべてのフレーミング機能が終了するため、パニックの拡大を止める唯一の方法は、recover()関数を呼び出すことです。 遅延式とパニック/回復機能の使用を組み合わせることにより、try / catch構造を使用してオブジェクト指向言語で達成されるのと同じセキュリティを実現できます。 特に、リソースの漏洩とプログラムの予期しない終了を防ぐため。



構造のインスタンスの破壊の瞬間が予測できない場合、Go to free resourcesの唯一の方法は、標準ランタイムパッケージのSetFinalizer関数を使用することです 。 インスタンスがガベージコレクターによってクリアされる瞬間をキャッチできます。



設計パターン



したがって、説明したメカニズムにより、オブジェクト指向プログラミングでの継承、カプセル化、多相性の解決と同じ問題を解決できます。 インターフェイスと組み合わせたアヒルのタイピングの存在は、オブジェクト指向言語の従来の継承とほぼ同じ可能性を提示します。 これは、以下のいくつかの主要な古典的な設計パターンの実装によってよく示されています。



シングルトン-シングルトン



Goには静的修飾子はありません。静的変数が必要な場合は、パッケージ本体で実行されます。 シングルトンパターンは、最も単純な場合、このソリューションに基づいて構築されます。



 type Singleton struct{ } //         var instance *Singleton func GetSingletonInstance() *Singleton { if instance == nil { instance = &Singleton{} } return instance }
      
      







抽象的な工場。 ファクトリーメソッド。 ビルダー-抽象的な工場。 ファクトリーメソッド。 ビルダー



3つのパターンはすべて、何らかの抽象インターフェイスの実装に基づいています。これにより、独自のメソッド作成者を実装することにより、特定の製品の作成を制御できます。 インターフェイス宣言は次のようになります。



 type AbstractProduct interface{ } //   type AbstractFactory interface { CreateProduct1() AbstractProduct CreateProduct2() AbstractProduct } //   type AbstractCreator interface { FactoryMethod() AbstractProduct } //  type AbstractBuilder interface { GetResult() AbstractProduct BuildPart1() BuildPart2() }
      
      





特定の構造の1対1メソッドの実装は、オブジェクト指向プログラミングの実装に対応しています。



例はgithubで見ることができます:



抽象ファクトリー

工場法

ビルダー



プロトタイプ-プロトタイプ



多くの場合、Prototypeパターンは単純に構造の浅いコピーに置き換えられます。



 type T struct{ Text string } func main(){ proto := &T{"Hello World!"} copied := &T{} //   *copied = *proto if copied != proto { fmt.Println(copied.Text) } }
      
      







一般的な場合、問題はCloneメソッドを使用してインターフェイスを作成することにより、古典的な方法で解決されます。



 type Prototype interface{ Clone() Prototype }
      
      







実装例はgithub: Prototypeにあります。



RAII



RAIIパターンの使用は、デストラクタが存在しないため複雑になります。そのため、許容できる動作を多かれ少なかれ得るには、runtime.setFinalizer関数を使用する必要があります。



 type Resource struct{ } func NewResource() *Resource { //     runtime.SetFinalizer(r, Deinitialize) return r } func Deinitialize(r *Resource) { //    }
      
      







実装例:



RAII



アダプター デコレータ。 橋。 ファサード-アダプター。 橋 デコレータ 正面



4つのパターンはすべて非常に似ており、同じ方法で構成されているため、アダプターの実装のみを提供するだけで十分です。



 type RequiredInterface interface { MethodA() } type Adaptee struct { } func (a *Adaptee) MethodB() { } type Adapter struct{ Impl Adaptee } func (a *Adapter) MethodA() { a.Impl.MethodB() }
      
      







リンカー-複合



リンカの実装はさらに簡単です。 Composite(構造的な動作を記述する)とComponent(ユーザー定義関数を記述する)の2つのインターフェイスだけで十分です。



 type Component interface { GetName() string } type Composite interface { Add(c Component) Remove(c Component) GetChildren() []Component }
      
      





パターンの実装例: Linker



責任の連鎖-責任の連鎖



Goの非常に一般的なパターンですが、主に匿名関数ハンドラーを介して実装されます。 それらは、たとえばパッケージnet / http標準ライブラリなど、多数あります。 クラシックバージョンでは、パターンは次のようになります。



 type Handler interface{ Handle(msg Message) } type ConcreteHandler struct { nextHandler Handler } func (h *ConcreteHandler) Handle(msg Message) { if msg.type == "special_type" { // handle msg } else if next := h.nextHandler; next != nil { next.Handle(msg) } }
      
      







実装例: 責任の連鎖



Nice Goの機能



示されているように、ほとんどすべての古典的なデザインパターンを言語で再現できます。 ただし、これはこの言語の主な利点ではありません。 ゴルーチンベースのマルチスレッドのサポート、スレッド間のデータチャネル、匿名関数とコンテキストクロージャのサポート、Cライブラリとの容易な統合、および強力な標準パッケージライブラリが非常に重要です。 これはすべて、個別の注意深い検討の価値がありますが、もちろん記事の範囲を超えています。



当然のことながら、この言語には他のイノベーションもあります。これは、言語自体よりも言語のインフラストラクチャにより関連しています。 ただし、経験豊富なプログラマなら誰でも感謝します。



git、hg、svn、bazaarをサポートするビルトインパッケージマネージャー



Goでは、すべてがパッケージに分割されます。Javaの場合と同様に、すべてがクラスに分割されます。 プログラムの実行を開始するメインパッケージは、mainと呼ばれます。 通常、各パッケージはプログラムの多かれ少なかれ独立した部分であり、メインからインポートまで含まれます。 たとえば、標準の数学パッケージを使用するには、 import“ math”と入力するだけです。 パッケージへのパスは、リポジトリのアドレスでもあります。 シンプルなOpenGLプログラムは次のようになります。



 package main import ( "fmt" glfw "github.com/go-gl/glfw3" ) func errorCallback(err glfw.ErrorCode, desc string) { fmt.Printf("%v: %v\n", err, desc) } func main() { glfw.SetErrorCallback(errorCallback) if !glfw.Init() { panic("Can't init glfw!") } defer glfw.Terminate() window, err := glfw.CreateWindow(640, 480, "Testing", nil, nil) if err != nil { panic(err) } window.MakeContextCurrent() for !window.ShouldClose() { //Do OpenGL stuff window.SwapBuffers() glfw.PollEvents() } }
      
      







すべての依存関係をダウンロードするには、プロジェクトディレクトリから取得します。



Local Goドキュメント



godocコマンドを使用して、コマンドラインからドキュメントをいつでも読むことができます。 たとえば、mathパッケージからSin関数の説明を取得するには、godoc math sinコマンドを入力します。



 $ godoc math Sin func Sin(x float64) float64 Sin returns the sine of the radian argument x. Special cases are: Sin(±0) = ±0 Sin(±Inf) = NaN Sin(NaN) = NaN
      
      







また、ローカルマシンでは、インターネットが何らかの理由で利用できなくなった場合にgolang.comサーバークローンを開始できます。



 $ godoc -http=:6060
      
      







godocの詳細をご覧ください。



コマンドラインのリファクタリングとフォーマット



コードでは、たとえば、パターンを使用して名前を変更したり、同種の数式を修正したりするなど、均一な変更を行う必要がある場合があります。 このためにgofmtツールが提供されています。



 gofmt -r 'bytes.Compare(a, b) == 0 -> bytes.Equal(a, b)'
      
      







bytes.Compare(a、b)という形式のすべての式をbytes.Equal(a、b)に置き換えます。 変数が異なる方法で呼び出される場合でも。



Gofmtは、-sフラグを使用して一般的な式を簡素化するためにも使用できます。 このフラグは、次の置換に似ています。



 []T{T{}, T{}} -> []T{{}, {}} s[a:len(s)] -> s[a:] for x, _ = range v {...} -> for x = range v {...}
      
      







また、gofmtを使用して、プロジェクトのコードスタイルを保存できます。 gofmtの詳細



単体テストとベンチマーク



Goには、特別なテストテストパッケージが含まれています。 パッケージのテストを作成するには、サフィックス「_testing.go」を付けて同じ名前のファイルを作成します。 すべてのテストとベンチマークは、テストまたはベンチで始まります。



 func TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } ... } func BenchmarkHello(b *testing.B) { for i := 0; i < bN; i++ { fmt.Sprintf("hello") } }
      
      







テストを実行するには、go testユーティリティを使用します。 これを使用して、テストの実行、カバレッジの測定、ベンチマークの実行、またはパターンのテストの実行を行うことができます。 この記事のパターンを例として記述およびテストするために作成されたgopatternsプロジェクトを使用すると、次のようになります。



 $ go test -v === RUN TestAbstractFactory --- PASS: TestAbstractFactory (0.00 seconds) === RUN TestBuilder --- PASS: TestBuilder (0.00 seconds) === RUN TestChain --- PASS: TestChain (0.00 seconds) === RUN TestComposite --- PASS: TestComposite (0.00 seconds) === RUN TestFactoryMethod --- PASS: TestFactoryMethod (0.00 seconds) === RUN TestPrototype --- PASS: TestPrototype (0.00 seconds) === RUN TestRaii --- PASS: TestRaii (1.00 seconds) === RUN TestSingleton --- PASS: TestSingleton (0.00 seconds) PASS ok gopatterns 1.007s $ go test -cover PASS coverage: 92.3% of statements $go test -v -run "Raii" === RUN TestRaii --- PASS: TestRaii (1.00 seconds) PASS ok gopatterns 1.004s
      
      







おわりに



したがって、Goは命令型パラダイムに基づいて構築されているという事実にもかかわらず、従来のデザインパターンを実装するのに十分な資金があります。 この点で、一般的なオブジェクト指向言語よりも決して劣っていません。 同時に、組み込みのパッケージマネージャー、言語インフラストラクチャレベルでの単体テストのサポート、組み込みのリファクタリングおよび文書化ツールなどにより、言語と競合他社を大幅に区別します。 このようなものは通常、コミュニティによって実装されます。



これらすべては、ゴルーチン、チャネル、ネイティブライブラリとのインターフェイスの詳細な調査なしでも可能です。



一般に、Goは、命令プログラミングと構造プログラミングが歴史の中で下がらないことを示しています。 ソフトウェア開発の主な傾向に適合する現代言語は、命令型パラダイムに基づいて構築できますが、オブジェクト指向または機能的パラダイムに基づいた場合よりも悪くはなりません。



All Articles