Go の基礎 11 - Go 言語のパッケージインポートを理解する

Go 言語はパッケージをソースコードの基本単位としており、複数のパッケージをリンクすることで Go プログラムを構築すると言えます。これは、Java や Python などの言語に比べて革新的なものではありませんが、祖先である C 言語のヘッダー ファイル インクルードのメカニズムよりもはるかに「高度」です。

すべてのコンパイルが最初から開始される場合でも、コンパイル速度の速さは、この「進歩」の顕著な現れです。基本的な構築単位としてパッケージを使用する Go 言語の構築モデルにより、依存関係の分析が非常に簡単になり、C 言語のヘッダー ファイルを介して依存関係を分析するという膨大なオーバーヘッドが回避されます。

Go のコンパイルが速い理由は、次の 3 つの側面に反映されています。

● Go では、Go コンパイラが依存パッケージのリストを決定するためにファイル全体を読み取って処理する必要がないように、各ソース ファイルが最初にすべての依存パッケージ インポートを明示的にリストする必要があります。

● Go では、パッケージ間に循環依存関係が存在できないことが必要であるため、パッケージの依存関係は有向非循環グラフを形成します。ループがないため、パッケージは個別にコンパイルすることも、並行してコンパイルすることもできます。

● コンパイルされた Go パッケージに対応するターゲットファイル (file_name.o または package_name.a) には、パッケージ自体のエクスポート シンボル情報だけでなく、依存するパッケージのエクスポート シンボル情報も記録されます。

このようにして、Go コンパイラーが特定のパッケージ P をコンパイルするとき、P が依存するパッケージごとに 1 つのターゲット ファイルを読み取るだけで済みます (パッケージ Q のインポートなど) (たとえば、Q パッケージにコンパイルされたターゲット ファイルにはすでに Q が含まれています)他のファイルの情報を読み取らずに、パッケージの依存パッケージの情報をエクスポートします。
Go 言語でのパッケージの定義と使用は非常に簡単です。package キーワードを使用して、Go ソース ファイルが属するパッケージを宣言します。

// xx.go
package a
...
上述源码表示文件xx.go是包a的一部分。
使用import关键字导入依赖的标准库包或第三方包:
package main
	import (
	"fmt" // 标准库包导入
	"a/b/c" // 第三方包导入
)
func main() {
    
    
c.Func1()
fmt.Println("Hello, Go!")
}

多くの Gopher が上記のコードを見ると、import 後の「c」と「fmt」、および c.Func1() と fmt.Println() の c と fmt が同じ構文要素であることを当然のことと考えるでしょう。名前。しかし、Go 言語を深く勉強すると、これが当てはまらないことが誰でも分かるでしょう。

たとえば、リアルタイム分散メッセージング フレームワーク nsq によって提供される公式クライアント パッケージを使用する場合、パッケージのインポートは次のように記述されます。

import "github.com/nsqio/go-nsq"

ただし、このパッケージによって提供されるエクスポートされた関数を使用する場合、go-nsq.xx は使用しません。

nsq.xxx:
q, _ := nsq.NewConsumer("write_test", "ch", config)

多くの Gopher は、Go パッケージのインポートを学習するときにいくつかの疑問を抱きます。インポート後のパスの最後のセグメントは何を表しているのでしょうか? パッケージ名ですか、それともパスですか?

この記事では、私は皆さんと一緒に Go 言語のパッケージ インポートについて深く調査し、理解していきたいと思います。

Go プログラムの構築プロセス

まず、Go パッケージのインポートを理解する前に、Go プログラムの構築プロセスを簡単に見てみましょう。静的にコンパイルされる主流の言語と同様、Go プログラムの構築は、コンパイルとリンクの 2 つの段階で構成されます。

メイン以外のパッケージはコンパイル後に .a ファイルを生成しますが、これは Go パッケージのターゲット ファイルとして理解でき、
ターゲット ファイルは実際にはパック ツール ( GOROOT/pkg/tool/darwinamd 64/pack) を通じて処理されます。 o ファイルがパッケージ化された後に形成されます。デフォルトでは、.o ファイルが goinstall を使用してパッケージ化されていない限り (GOROOT/pkg/tool/darwin_amd64/pack にインストールされている)、コンパイル プロセス中に .a ファイルが一時ディレクトリに生成されます。デフォルトでは、 go install を使用してインストールしない限り、.a ファイルはコンパイル プロセス中に一時ディレクトリに生成されます。GOROOT / p kg /トゥール/ダーウィン_ _ _ _ _ _ _ _m d 64/ pack )は、 .oファイルをパッケージ化した後に形成されますデフォルトでは、.aファイルはコンパイル プロセス中に一時ディレクトリに生成されます。go in s t a ll を使用して GOPATH/pkg (Go 1.11 より前) にインストールしない限り、.aファイル見ることできませ実行可能プログラムをビルドしている場合、.a ファイルは実行可能プログラムをビルドするリンク段階から使用されます。

標準ライブラリパッケージのソースコードファイルはGOROOT/src以下に、対応する.aファイルはGOROOT/src以下に、対応する.aファイルは以下に格納されます。GOROOT / src に保存され、対応する.aファイルはGOROOT/pkg/ darwin_amd64 に保存されます
(例として macOS を使用します。Linux システムの場合は linux_amd64 です)。

// Go 1.16
$tree -FL 1 $GOROOT/pkg/darwin_amd64
├── archive/
├── bufio.a
├── bytes.a
├── compress/
├── container/
├── context.a
├── crypto/
├── crypto.a
...

「より深い説明を求めている」読者は、次の質問をするかもしれません: Go プログラムをビルドするとき、コンパイラーは依存パッケージのソース ファイルを再コンパイルしますか、それともパッケージの .a ファイルを直接リンクしますか? 実験を通して答えを出しましょう。

Go 1.10 バージョンではビルド キャッシュが導入されましたが、ビルド キャッシュによって実験プロセスと分析が複雑になるのを避けるために、この実験では Go 1.9.7 バージョンを使用しました。
実験環境のディレクトリ構成は以下のとおりです。

chapter3-demo1
├── cmd/
│ └── app1/
│ └── main.go
└── pkg/
└── pkg1/
└── pkg1.go

デモンストレーションのみを目的としているため、pkg1.go と main.go のソース コードは非常に単純です。

// cmd/app1/main.go
package main
import (
"github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
)
func main() {
    
    
pkg1.Func1()
}
// pkg/pkg1/pkg1.go
package pkg1
import "fmt"
func Func1() {
    
    
fmt.Println("pkg1.Func1 invoked")
}

Chapter3-demo1 と入力し、次のコマンドを実行します。

$go install github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1

その後、できることは、

$GOPATH/pkg/darwin_amd64/github.com/bigwhite/effective-
go-book/chapter3-demo1/pkg下看到pkg1包对应的目标文件pkg1.a:
$ls $GOPATH/pkg/darwin_amd64/github.com/bigwhite/effective-go-book/chapter3-demo1/pkg
pkg1.a

実行可能プログラム app1 を、chapter3-demo1 パスの下でコンパイルし続けます。

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1

上記のコマンドを実行すると、chapter3-demo1 の下に実行可能ファイル app1 が表示されるので、そのファイルを実行します。

$ls
app1* cmd/ pkg/
$./app1
pkg1.Func1 invoked

これは私たちの予想と一致していますが、現時点では出力に一貫性があるため、pkg1 パッケージのソース コードまたはターゲット ファイル pkg1.a のどちらが app1 のコンパイルに使用されたのかを知ることはできません。

pkg1.go のコードを変更します。

// pkg/pkg1/pkg1.go
package pkg1
import "fmt"
func Func1() {
    
    
fmt.Println("pkg1.Func1 invoked - Again")
}

app1 を再コンパイルして実行すると、次の結果が得られます。

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app1
$./app1
pkg1.Func1 invoked - Again

パス名ですか、それともパッケージ名ですか?

これまでの実験を通じて、コンパイラは、コンパイル プロセス中にコンパイル単位 (パッケージ) が依存するパッケージのソース コードを使用する必要があることがわかりました。

コンパイラが依存パッケージのソース コード ファイルを見つけるには、依存パッケージのソース コード パスを知る必要があります
このパスは、ベース検索パスとパッケージ インポート パスの 2 つの部分で構成されます

基本的な検索パスはグローバル設定であり、そのルールについては以下で説明します。
1) すべてのパッケージ (標準ライブラリ パッケージかサードパーティ パッケージかを問わず) のソース コード ベースの検索パスにはGOROOT/src が含まれます。2) 上記の基本検索パスに基づいて、Go のバージョンが異なれば、他の基本検索パスも異なります。● Go バージョン 1.11 より前では、パッケージのソース コードの基本検索パスには GOROOT/src も含まれていました。2) 上記の基本検索パスに基づいて、Go のバージョンが異なれば、他の基本検索パスも異なります。● Go バージョン 1.11 より前では、パッケージのソース コードの基本検索パスも含まれていました。ゴルート/ソース. 2 ) 上記の基本検索パスに基づいて、Goのバージョンが異なれば、他の基本検索パスも異なりますGoバージョン1.11より前では、パッケージのソース コードの基本検索パスにはGOPATH /src も含まれていました。
● Go 1.11 ~ Go 1.12 バージョンでは、パッケージ ソース コードの基本検索パスに 3 つのモードがあります。
● クラシック gopath モード (GO111MODULE=off):GOPATH / src。● モジュール認識モード (GO 111 MODULE = オン): GOPATH/src。● モジュール対応モード (GO111MODULE=on) の場合:GOPATH / src module _ _ _ _ _ _ _ _ _ウェアモード( GO 111 MOD ULE _ _ _=o n ): GOPATH/pkg/mod.
● 自動モード (GO111MODULE=auto):GOPATH/src パスの下では gopath モードと同じであり、GOPATH/src パスの下では gopath モードと同じです。GOP A T H / srcパスの下ではgo pathモードと同じですが、 GOP A T H / src パスの外側および go.mod を含む場合、モジュール対応モードと同じです。
● Go バージョン 1.13 では、パッケージ ソース コードの基本検索パスに 2 つのモードがあります。
● クラシック gopath モード (GO111MODULE=off):GOPATH / src。● モジュール認識モード (GO 111 MODULE = オン / 自動): GOPATH/src。● モジュール対応モード (GO111MODULE=on/auto) の場合:GOPATH / src module _ _ _ _ _ _ _ _ _ウェアモード( GO 111 MOD ULE _ _ _=o n / a u to o ): GOPATH/pkg/mod.
● 将来の Go バージョンにはモジュール対応モードのみが含まれる予定です。つまり、パッケージのソース コードはモジュール キャッシュ ディレクトリ内でのみ検索されます。
検索パスの 2 番目の部分は、各パッケージ ソース ファイルの先頭にあるパッケージ インポート パスです。基本検索パスと
パッケージ インポート パスを組み合わせると、Go コンパイラは、パッケージのすべての依存パッケージのソース コード パスのセットを決定できます。このセットは、
Go コンパイラのソース コード検索パス空間を構成します。次の例を考えてみましょう。

// p1.go
package p1
import (
"fmt"
"time"
"github.com/bigwhite/effective-go-book"
"golang.org/x/text"
"a/b/c"
"./e/f/g"
)
...

パッケージ名の競合の問題

同じパッケージ名が異なるプロジェクトや異なるウェアハウスに存在する場合があります。同じソース コード ファイルには、
そのパッケージ インポート パスによって形成されるソース コード検索パス空間に同じ名前のパッケージが存在する可能性があります。たとえば、別の Chapter3-demo2 があり、
これにも pkg1 という名前のパッケージがあり、インポート パスは github.com/bigwhite/Effective-go-book/chapter3-demo2/pkg/pkg1 です
cmd/app3 が Chapter3-demo1 と Chapter3-demo2 の pkg1 パッケージを同時にインポートするとどう
なりますか?

// cmd/app3
package main
import (
"github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
"github.com/bigwhite/effective-go-book/chapter3-demo2/pkg/pkg1"
)
func main() {
    
    
pkg1.Func1()
}

cmd/app3 をコンパイルします。

$go build github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app3
# github.com/bigwhite/effective-go-book/chapter3-demo1/cmd/app3
./main.go:5:2: pkg1 redeclared as imported package name
previous declaration at ./main.go:4:2

パッケージ名の競合の問題が実際に発生していることがわかります。この問題を解決するにはどうすればよいでしょうか? または、
パッケージのインポート パスでパッケージのパッケージ名を明示的に指定する方法を使用します。

package main
import (
pkg1 "github.com/bigwhite/effective-go-book/chapter3-demo1/pkg/pkg1"
mypkg1 "github.com/bigwhite/effective-go-book/chapter3-demo2/pkg/pkg1"
)
func main() {
    
    
pkg1.Func1()
mypkg1.Func1()
}

上記の pkg1 は、chapter3-demo1/pkg/pkg1 の下のパッケージを指し、mypkg1 は、chapter3-demo1/pkg/pkg1 の下のパッケージを指します。この時点で、パッケージ名の競合の問題は簡単に解決されます。

実験を通じて Go 言語のパッケージ インポートについてさらに理解しましたが、Gopher は次の結論を念頭に置く必要があります。

● Go コンパイラは、コンパイル プロセス中にコンパイル単位 (パッケージ) が依存するパッケージのソース コードを使用する必要があります。

● Go ソースコードファイルの先頭にある package import 文の import 以降の部分はパスであり、パスの最後の部分はパッケージ名ではなくディレクトリ名です。

● Go コンパイラのパッケージ ソース コード検索パスは、基本検索パスとパッケージ インポート パスで構成され、これら 2 つを組み合わせると、コンパイラはパッケージのすべての依存パッケージのソース コード パスのセットを決定できます。 Go コンパイラのソース コード検索パス スペース。

● 同じソースコード検索パス空間内の同じソースコードファイルの依存パッケージ間でパッケージ名が競合する問題は、パッケージ名を明示的に指定することで解決できます。

おすすめ

転載: blog.csdn.net/hai411741962/article/details/132753453