要約: この記事は元々、Grape City の技術チームによって CSDN に公開されたものです。転載元を明記してください:グレープシティ公式ウェブサイト、グレープシティは、開発者に力を与えるための専門的な開発ツール、ソリューション、サービスを開発者に提供します。
序文
この記事では主に、値の受け渡しとポインタ、文字列、配列、スライス、コレクション、オブジェクト指向(カプセル化、継承、抽象化)、設計思想の7つの側面からGO言語の特徴を紹介します。
記事ディレクトリ:
2.7.2. Go にはデフォルトのパラメータもメソッドのオーバーロードもありません
1. 囲碁の過去と現在
1.1 Go言語の誕生過程
2007 年 9 月のある日、Google エンジニアのロブ・パイク氏はいつものように C++ プロジェクトの構築を開始したと言われていますが、これまでの経験によれば、この構築には 1 時間ほどかかるはずです。この時、彼と他の 2 人の Google 同僚、ケン トンプソンとロバート グリーズマーは不平を言い始め、新しい言語についてのアイデアを表明しました。当時、Google は社内でさまざまなシステムを構築するために主に C++ を使用していましたが、C++ の複雑さと並行性のネイティブ サポートの欠如により、3 つの大きな上司は非常に悩みました。
初日の雑談は有意義で、プログラマに喜びをもたらし、将来のハードウェア開発トレンドに適合し、Google 社内の大規模ネットワーク サービスを満足させる新しい言語をすぐに思いつきました。そして二日目に彼らは再び集まり、新しい言語について真剣に考え始めました。翌日の会議の後、ロバート・グリーズマーは次のような電子メールを送信しました。
電子メールから、この新しい言語に対する彼らの期待は次のとおりであることがわかります。 **C 言語をベースにして、いくつかのエラーを修正し、いくつかの批判された機能を削除し、いくつかの不足している機能を追加します。**Switch ステートメントの修復、import ステートメントの追加、ガベージ コレクションの追加、インターフェイスのサポートなど。そして、このメールが Go の設計の最初の草案となりました。
この数日後、ロブ・パイクは車で帰宅する途中、新しい言語に Go という名前を思いつきました。彼の頭の中では、「Go」という単語は短く、入力しやすく、Go のツールチェーン (goc コンパイラー、goa アセンブラ、gol リンカーなど) など、その後の他の文字と簡単に組み合わせることができます。また、この単語は彼らの言葉にも適合します。言語設計の本来の意図: シンプル。
1.2 ステップバイステップの成形
Go の設計思想を統一した後、Go 言語は言語の設計の反復と実装を正式に開始しました。2008 年に、C 言語の父である Ken Thompson が Go コンパイラの最初のバージョンを実装しました。このバージョンの Go コンパイラは今でも C 言語で開発されています。その主な動作原理は、Go を C にコンパイルしてからコンパイルすることです。 C をバイナリに変換します。2008 年半ばまでに、Go の設計の最初のバージョンはほぼ完成しました。このとき、同じく Google で働いていた Ian Lance Taylor は、Go 言語の 2 番目のコンパイラでもある Go 言語用の gcc フロントエンドを実装しました。イアン・テイラーのこの功績は励ましであるだけでなく、新しい言語である Go の実現可能性の証明でもあります。言語の 2 番目の実装では、Go の言語仕様と標準ライブラリを確立することも重要です。その後、Ian Taylor は Go 言語開発チームの 4 人目のメンバーとして正式に加わり、後に Go 言語の設計と実装における中心人物の 1 人になりました。Russ Cox は Go コア開発チームの 5 人目のメンバーで、これも 2008 年に加わりました。チームに参加した後、Ross Cox は、関数型が「第一級市民」であり、独自のメソッドを持つこともできるという特徴を利用して、http パッケージの HandlerFunc 型を巧みに設計しました。このようにして、明示的な変換を通じて、通常の関数を http.Handler インターフェイスを満たす型にすることができます。それだけでなく、Ross Cox は、Go 言語の I/O 構造モデルを確立した io.Reader インターフェイスや io.Writer インターフェイスなど、当時の設計に基づいたより一般的なアイデアも提案しました。その後、Ross Cox は Go コア技術チームの責任者となり、Go 言語の継続的な進化を推進しました。この時点で、Go 言語の初期コアチームが形成され、Go 言語は安定した進化の道を歩み始めています。
1.3が正式リリースされました
2009 年 10 月 30 日、Rob Parker は Google Techtalk で Go 言語に関する講演を行い、これが Go 言語が初めて公開されました。10 日後の 2009 年 11 月 10 日、Google は Go 言語プロジェクトがオープンソースであることを正式に発表し、この日が Go によって Go 言語の誕生の日として正式に指定されました。
(Go言語マスコット Gopher)
1.4. Go インストールガイド
1. Go言語インストールパッケージのダウンロード
Go公式サイト:https://golang.google.cn/
対応するインストール バージョンを選択するだけです (.msi ファイルを選択することをお勧めします)。
2. インストールが成功したかどうか、および環境が正常に構成されているかを確認します。
コマンド ラインを開きます: win + R を押して実行ボックスを開き、cmd コマンドを入力してコマンド ライン ウィンドウを開きます。
コマンドラインに go version と入力するとインストールされているバージョンが表示され、以下の内容が表示されればインストール成功です。
[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-nhDTdQKK-1689067489257)(media/54576b6c7593e8c73a97975be0910b4b.png)]
2. Go言語の特別な言語機能
2.1 値とポインタによる受け渡し
Go の関数パラメータと戻り値はすべて値渡しです。それはどういう意味ですか?たとえば、次のコード:
type People struct {
name string
}
func ensureName(p People) {
p.name = "jeffery"
}
func main() {
p := People{
name: ""
}
ensurePeople(p)
fmt.Println(p.name) // 输出:""
}
上記のコードでは、p の内容が「jeffery」に変更されないのはなぜですか? Go 言語の値渡し機能により、ensureName 関数で受け取った p は、すでに main 関数の p のコピーになっています。これは、C# で p を int 型に変更した場合と同じ結果です。
どうやって解決すればいいでしょうか?ポインタを使用します。
他の人はどうなのか知りませんが、初めて Go を習い、ポインタを学ぶ必要があると知ったとき、大学時代に C や C++ のポインタに苦しめられた苦い記憶が瞬時に思い出され、本能的に拒否感を覚えました。ポインターは C# でも使用できますが、基礎となるコードを作成しない場合、コードを作成するのは 10 年に 1 回未満になる可能性があります。
幸いなことに、Go でのポインターの使用は簡素化されており、複雑なポインター計算ロジックはなく、次の 2 つの操作を知っているだけでポインターを簡単に使用できます。
- "*": アドレスの内容を取得します
- "&": 変数のアドレスを取得します
var p \*People = \&People{
name: "jeffery",
}
上記のコードでは、新しい People インスタンスを作成し、「&」操作を通じてそのアドレスを取得し、そのアドレスを *People のポインター型変数 p に割り当てました。このとき、p はポインタ型です。C や C++ では People ではフィールド名を直接操作できませんが、Go ではポインタ操作が簡略化され、ポインタ型変数のフィールドを直接操作できます。
func main() {
fmt.Println(p.name) // 输出:jeffery
fmt.Println(\*(p).name) // 输出:jeffery
}
上記 2 つの操作は同等です。
ポインターを使用すると、パラメーターを参照渡しするC# コードを簡単にシミュレートできます。
type People struct {
name string
}
func ensureName(p \*People) {
p.name = "jeffery"
}
func main() {
p := \&People{
name: ""
}
ensurePeople(p)
fmt.Println(p.name) // 输出:jeffery
}
2.2 文字列
C# では、文字列は実際には char 型の配列であり、スタック領域に割り当てられる特別な参照型です。
Go 言語では、文字列は値の型であり、文字列は全体です。つまり、文字列の内容を変更することはできません。この概念は、次の例から明確にわかります。
var str = "jeffery";
str[0] = 'J';
Console.WriteLine(str); // 输出:Jeffery
上記の構文は C# で有効です。変更するのは実際には文字列内の char 型であり、Go でのそのような構文はコンパイラによってエラーとして報告されるためです。
str := "jeffery"
str[0] = 'J' // 编译错误:Cannot assign to str[0]
ただし、配列インデックスを使用して、対応する文字列の値を読み取ることができます。
s := str[0]
fmt.Printf("%T", s) // uint8
戻り値が uint8 であることがわかりますが、これはなぜでしょうか? 実はGoでは文字列型はruneという型で構成されており、Goのソースコードを入力するとruneの定義がint64型であることがわかります。これは、Go では文字列が 1 つずつ UTF8 コードにコンパイルされ、各ルーンは実際には UTF8 コードに対応する値であるためです。
さらに、文字列型にはピットがあります。
str := "李正龙"
fmt.Printf("%d", len(str))
len() 関数は go の組み込み関数でもあり、コレクションの長さを見つけるために使用されます。
Go では中国語が UTF-8 エンコーディングにコンパイルされ、中国語の文字のエンコーディング長が 3 であるため、上記の例では 9 が返されます。そのため、3 つの中国語文字は 9 になりますが、一部の特殊な中国語文字があるため、必ずしもそうとは限りません。これは 4 つの長さを占めるため、単純に len() / 3 を使用してテキストの長さを取得することはできません。
したがって、漢字の長さを求める方法は次のようになります。
fmt.Println(utf8.RuneCountInString("李正龙"))
2.3 配列
Go の配列も、少し低レベルすぎる概念だと思います。基本的な使い方は C# と同じですが、細部は大きく異なります。
まず第一に、Go の配列も値型であり、さらに、配列は連続したメモリの組み合わせであるという概念に「厳密に」従っているため、配列の長さは配列の一部になります。これはスライスを直接区別する機能であるため、この概念も重要です。さらに、Go の配列の長さは定数のみです。
a := [5]int{
1,2,3,4,5}
b := [...]{
1,2,3,4,5}
lena := len(a)
lenb := len(b)
上記は、Go における配列の比較的従来的な 2 つの初期化構文です。配列の長さは文字列の長さと同じで、組み込み関数 len() を通じて取得されます。残りの使い方は基本的に C# と同じで、たとえば、インデックスによって値を割り当てることができ、トラバースすることができ、値を挿入することはできません。
2.4 スライス
配列に相当する概念として、Go 独自の Slice 型があります。配列にはスケーラビリティがないため、日常の開発では配列が使用されることはほとんどありません。たとえば、C# では配列を使用することはほとんどなく、配列が使用できる場合は基本的に List<T> が使用されます。Slice は Go 言語で実装された List であり、参照型であり、主な目的は配列がデータを挿入できない問題を解決することです。その最下層も配列ですが、配列をカプセル化し、配列の左右の境界を指す 2 つのポインターを追加することで、Slice にデータを追加する機能を持たせます。
s1 := []int{
1,2,3,4,5}
s2 := s1[1:3]
s3 := make([]int, 0, 5)
上記は、Slice の一般的に使用される 3 つの初期化方法です。
- スライスと配列の唯一の違いは、配列定義に数量がないことであることがわかります。
- 1 つのスライスに基づいて別のスライスを作成できます。その後ろの数字の意味は、現在の業界全体の左を含む右を閉じたものです。
- スライスは **make()** 関数で作成できます
make() 関数は、Go 開発者の生活に付属しているように感じられます。Go の 3 つの参照型はすべて、make 関数を通じて初期化および作成されます。スライスの場合、最初のパラメータはスライス タイプを示します。たとえば、上記の例では int タイプのスライスを初期化し、2 番目のパラメータはスライスの長さを示し、3 番目のパラメータはスライスの容量を示します。
データをスライスに挿入したい場合は、append() 関数を使用する必要がありますが、その構文は非常に奇妙で、とんでもないことだと言えます。
s := make([]int)
s = append(s, 12345) // 这种追加值还需要返回给原集合的语法真不知道是哪个小天才想到的
ここで、スライスの容量という新しい概念が登場します。配列には容量の概念がないことはわかっています (実際にはありますが、容量は長さです)。また、スライスの容量は実際には C# の List<T> の容量と似ています (ほとんどの C #ers は List を使用しています (Capacity パラメーターをまったく気にしない場合もあります)。容量は基になる配列の長さを示します。
容量はcap()関数で取得できます。
C# では、List のデータが基になる配列を満たす場合、拡張操作が発生し、元のデータを新しい配列にコピーするために新しい配列を作成する必要があります。これは非常にパフォーマンスを消費する操作であり、同じです。 Go では true です。したがって、日常の開発でリストまたはスライスを使用する場合、容量が事前に決定できる場合は、拡張によるパフォーマンスの低下を避けるために、初期化時に容量を定義することが最善です。
2.5 収集
Go のスライスとして組み込み List に加えて、マップ タイプとして Dictionary<TKey, TValue> も組み込みます。map は Go の 3 つの参照型の 2 番目です。これはスライスと同じ方法で作成され、make 関数も渡す必要があります。
m := make(map[int]string, 10)
文字通りの意味から、この文はキーが int、値が string、初期容量が 10 のマップ型を作成することがわかります。
マップ上の操作は C# ほど複雑ではなく、get、set、contains の操作はすべて [] によって実装されます。
m := make(map[string]string, 5)
// 判断是否存在
v, ok := m["aab"]
if !ok {
//说明map中没有对应的key
}
// set值,如果存在重复key则会直接替换
m["aab"] = "hello"
// 删除值
delete(m, "aab")
ここに落とし穴があります。Go のマップも横断することができますが、Go では結果の順序が強制的に変更されるため、横断するたびに結果が同じ順序で得られない可能性があります。
2.6 オブジェクト指向
2.6.1 梱包
いよいよオブジェクト指向になります。注意深い学生なら、Go にはカプセル化制御のキーワード public、protected、private がないことがわかったはずです。では、私のオブジェクト指向の第一原理のカプセル化はどうなるでしょうか?
Go 言語のカプセル化は、変数の最初の文字の大文字と小文字によって制御されます (コードのクリーン度を重視する私にとって、これは大きな恩恵です。小文字の属性を確認する必要がなくなりました)。
// struct类型的首字母大写了,说明可以在包外访问
type People struct {
// Name字段首字母也大写了,同理包外可访问
Name string
// age首字母小写了,就是一个包内字段
age int
}
// New函数大写了,包外可以调到
func NewPeople() People {
return People{
Name: "jeffery",
age: 28
}
}
2.6.2 継承
カプセル化は完了しましたが、継承はどうなるのでしょうか?Go には継承されたキーワード extends がないようです。Go は、デザインパターンの継承よりも合成を優先するという設計思想に基づいて再利用のロジックを完全に設計しており、Go には継承はなく、合成のみです。
type Animal struct {
Age int
Name string
}
type Human struct {
Animal // 如果默认不定义字段的字段名,那Go会默认把组合的类型名定义为字段名
// 这样写等同于: Animal Animal
Name string
}
func do() {
h := \&Human{
Animal: Animal{
Age: 19, Name: "dog"},
Name: "jeffery",
}
h.Age = 20
fmt.Println(h.Age) // 输出:20,可以看到如果自身没有组合结构体相同的字段,那可以省略子结构体的调用直接获取属性
fmt.Println(h.Name) // 输出:jeffery,对于有相同的属性,优先输出自身的,这也是多态的一种体现
fmt.Println(h.Animal.Name)// 输出:dog,同时,所组合的结构体的属性也不会被改变
}
この組み合わせの設計パターンは、継承による結合を大幅に軽減し、この点だけでも完璧な特効薬だと思います。
2.6.3 抽象化
キーワードの説明の部分で Go にはインターフェースがあることを見ましたが、Go のインターフェースはすべて暗黙的に実装されているため、インターフェースを実装するためのキーワードも実装されていません。
type IHello interface {
sayHello()
}
type People struct {
}
func (p \*People) sayHello() {
fmt.Println("hello")
}
func doSayHello(h IHello) {
h.sayHello()
}
func main() {
p := \&People{
}
doSayHello(p) // 输出:hello
}
上記の例の構造体 p はインターフェースとは何の関係もないことがわかりますが、主に Go のすべてのインターフェースが暗黙的に実装されているため、 doSayHello 関数によって通常どおり参照できます。(つまり、特定の依存パッケージの特定のインターフェイスを作成して突然実装することは実際に可能だと思います)
また、ここでは構文が異なり、関数キーワード func の後に、関数名は直接定義されず、構造体 p へのポインタが追加されます。このような関数は構造体の関数、またはより簡単に言うと C# のメソッドです。
デフォルトでは、構造体に関数を定義するためにポインタ型を使用しますが、もちろんポインタなしでポインタを使用することもできますが、その場合、関数によって変更された内容は元の構造体とはまったく無関係になります。したがって、一般的には何も考えずにポインターを使用するという原則に従います。
さて、カプセル化、継承、抽象化がありますが、ポリモーフィズムに関しては継承で見たことがありますが、Goも自分自身の同じ関数にまずマッチします、関数がない場合は親構造体の関数を呼び出します。 、デフォルトでは、すべての関数がオーバーライドされる関数です。
2.7 設計哲学
Go 言語の設計哲学は、少ないほど豊かです。この文が意味するのは、Go には単純な構文が必要であり、単純な構文には暗黙的なものより明示的なものも含まれるということです(インターフェイスの型は実際には疑問符でいっぱいです)。それはどういう意味ですか?
2.7.1. Go にはデフォルトの型変換がありません
var i int8 = 1
var j int
j = i // 编译报错:Cannot use 'i' (type int8) as the type int
もう 1 つの例は、デフォルトでは文字列型を int などの他の型と連結できないことです。たとえば、「n hello」 + 1 と入力すると、Go ではコンパイル エラーが報告されます。その理由は、Go の設計者は、この種の変換は暗黙的であると考えているためであり、Go は単純である必要があり、これらの変換を含めるべきではありません。
2.7.2. Go にはデフォルトのパラメータもメソッドのオーバーロードもありません
これも非常に面倒な言語機能です。オーバーロードはサポートされていないため、コードを作成するときは、繰り返しても名前が異なる多数の関数を作成する必要があります。一部の開発者がこの機能について特に Go 設計者に質問したところ、「Go の設計目標はシンプルさです。シンプルさの前提の下では、多少の冗長コードは許容されます」という答えが返されました。
2.7.3. Go は属性をサポートしていません
現在のジェネリックの不足とは異なり、Go のジェネリックは開発中の機能であり、開発する時間がありません。feature Attribute は Java におけるアノテーションであり、Go ではサポートしないことが明記されている言語機能です。
Java ではアノテーションはどの程度強力になるのでしょうか? 例として:
大規模インターネットがマイクロサービス アーキテクチャに移行しつつある時代において、分散マルチセグメント コミットと分散トランザクションは比較的大きな技術的障壁となっています。分散トランザクションを例にとると、複数のマイクロサービスは同じチームによって開発されているわけではなく、世界中に展開されている場合もあります。操作をロールバックする必要がある場合、他のすべてのマイクロサービスはロールバック メカニズムを実装する必要があります。これには、複雑なビジネス モデルだけでなく、より複雑なデータベース ロールバック戦略も含まれます (2PC、TCC、各戦略を別個のクラスとして教えることができます)。
こういうものを一から開発しようとすると、総合的に検討するのはほとんど困難です。言うまでもなく、このような複雑なコードをビジネス コードに結合すると、コードが非常に見苦しくなります。分散トランザクションは言うまでもありません。単純なメモリ キャッシュは私たちにとって非常にわかりにくいものです。コードでは、最初にキャッシュを読み取り、次にデータベースを読み取るコードがよく見られます。これはビジネスと完全に結合されており、まったく保守できません。
Spring Cloud では、コード ユーザーは単純なアノテーション (つまり C# の機能) @Transactional を使用でき、このメソッドはトランザクションをサポートするため、この複雑な技術レベルのコードはビジネス コードから完全に分離され、開発者は次のように記述するだけで十分です。ビジネスコードは通常のビジネスロジックに完全に準拠しており、トランザクションの問題を心配する必要はありません。
しかし、Go の設計者は、アノテーションが呼び出し中のコード ユーザーの心に深刻な影響を与えるとも信じています。なぜなら、アノテーションを追加すると、関数のまったく異なる機能が生じる可能性があるからです。これは、明示的な方がより優れているという Go の設計哲学と一致しています。暗黙的とは裏腹に、ユーザーの精神的負担は著しく増大しますし、Goの設計思想にもそぐわないものです(とんでもない…)
2.7.4. Go には例外がない
Go には例外の概念はありませんが、代わりにエラー メカニズムが提供されます。C# の場合、実行中のコードに問題がある場合、手動で例外をスローすることができ、呼び出し元は後続の処理のために対応する例外をキャッチできます。ただし、Go には例外はなく、代わりにエラー メカニズムが使用されます。エラーのメカニズムは何ですか? Go のほぼすべての関数には複数の戻り値があると前に述べたことを覚えていますか? なぜ戻り値がこれほど多いのでしょうか? はい、受信エラー用です。たとえば、次のコード:
func sayHello(name string) error {
if name == "" {
return errors.New("name can not be empty")
}
fmt.Printf("hello, %s\\n", name)
return nil
}
// invoker
func main() {
if err := sayHello("jeffery"); err != nil {
// handle error
}
}
このようなエラー機構は、実行プロセス中にすべてのコードが異常クラッシュしないことを保証する必要があります。各関数が正常に実行されたかどうかは、関数が返すエラー情報によって判断する必要があります。関数呼び出しでエラーが返された場合 == nil、このコードは問題ないことを説明します。それ以外の場合は、エラーを手動で処理する必要があります。
これは深刻な結果につながる可能性があります。すべての関数呼び出しは次のように記述する必要があります。
if err := function(); err != nil
そういう構造。(これが、VS2022 がコード AI 補完機能をサポートした後、インターネット上の熱いレビューがすべて Gopher に対して肯定的である理由です。) このエラー メカニズムは、Go がハッキングされる最悪の場所でもあります。
この時、友達が言ったはずなので、このまま対処せずに1/0のようなコードを作成したらどうなるでしょうか?
上記のようなコードを記述すると、最終的に Go パニックが発生します。私の現在の浅い理解では、パニックは実際には C# の例外の概念です。パニックが発生するとプログラムは完全にクラッシュするためです。Go 設計者は、おそらく最初の設計ですべてのエラーを使用する必要があると考えました。パニックがトリガーされた場合のエラー処理、プログラムが使用できないことを意味します。したがって、パニックは実際には取り返しのつかない間違った概念です。
しかし、大規模なプロジェクトでは、自分が書いたコードが絶対確実でパニックにならないわけではなく、参照している他のパッケージが私たちの知らない動作をしてパニックになる可能性が非常に高いです。 、最も典型的な例: Go の httpRequest 本文は 1 回しか読み取ることができず、読み取った後は失われます。使用する Web フレームワークがリクエストの処理時に Body を読み取る場合、結果を再度読み取るときにパニックが発生する可能性があります。
したがって、パニックを解決するために、Go には、recover() 関数もあります。一般的な使用法は次のとおりです。
func main() {
panic(1)
defer func() {
if err := recover(); err != nil {
fmt.Println("boom")
}
}
}
実際、Go には強力な競合相手である Rust がいます。Rust は、2010 年に Mozilla Foundation によって開発された言語です。C 言語に基づいて開発された Go と同様に、Rust は C++ に基づいて開発されました。現在、コミュニティには Go と Rust という 2 つの陣営があり、互いに議論したり、際限なくおしゃべりしたりしています。もちろん、すべてに特効薬はなく、すべては弁証法的思考で学び、理解する必要があります。
さて、上記の Go 文法を読んだ後、記憶を深めるために Go コードの演習をいくつか書きます。ちょうどいいので、Go 公式サイトにある小さなサンプルを実装して、このフィボナッチ数列の N 番目の数を計算するインターフェイスを自分で実装してみましょう。
type Fib interface {
// CalculateFibN 计算斐波那契数列中第N个数的值
// 斐波那契数列为:前两项都是1,从第三项开始,每一项的值都是前两项的和
// 例如:1 1 2 3 5 8 13 ...
CalculateFibN(n int) int
}
スペースの制限があるため、この小さな演習の答えを私の gitee に載せておきます。皆さんの訪問を歓迎します。