Relationship between a package statement and the directory of a .go file

huangapple go评论165阅读模式
英文:

Relationship between a package statement and the directory of a .go file

问题

看这个实验。

~/go/src$ tree -F
.
├── 1-foodir/
│   └── 2-foofile.go
└── demo.go

1 directory, 2 files

~/go/src$ cat demo.go 
package main

import (
	"fmt"
	"1-foodir"
)

func main() {
	fmt.Println(foopkg.FooFunc())
}

~/go/src$ cat 1-foodir/2-foofile.go 
package foopkg

func FooFunc() string {
	return "FooFunc"
}

~/go/src$ GOPATH=~/go go run demo.go 
FooFunc

我以为我们总是导入一个包名。但上面的例子显示,我们实际上导入的是包目录名("1-foodir"),但在调用该包中的导出名称时,我们使用在Go文件中声明的包名(foopkg.FooFunc)。

对于像我这样从Java和Python世界来的初学者来说,这很令人困惑,在那里目录名本身就是用来限定包中定义的模块/类的包名。

为什么在Go中我们在使用import语句和引用包中定义的名称时有这样的差异?你能解释一下关于Go的这些事情的规则吗?

英文:

See this experiment.

~/go/src$ tree -F
.
├── 1-foodir/
│   └── 2-foofile.go
└── demo.go

1 directory, 2 files

~/go/src$ cat demo.go 
package main

import (
	"fmt"
	"1-foodir"
)

func main() {
	fmt.Println(foopkg.FooFunc())
}

~/go/src$ cat 1-foodir/2-foofile.go 
package foopkg

func FooFunc() string {
	return "FooFunc"
}

~/go/src$ GOPATH=~/go go run demo.go 
FooFunc

I thought that we always import a package name. But the above example
shows that we actually import a package directory name ("1-foodir")
but while invoking exported names within that package, we use the
package name declared in the Go files (foopkg.FooFunc).

This is confusing for a beginner like me who comes from Java and Python world,
where the directory name itself is the package name used to qualify
modules/classes defined in the package.

Why is there a difference in the way we use import statement and
refer to names defined in the package in Go? Can you explain the rules
behind these things about Go?

答案1

得分: 33

如果你所说的是真的,那么你的函数调用实际上应该是1-foodir.FooFunc()而不是foopkg.FooFunc()。相反,Go语言会在2-foofile.go中看到包名,并将其导入为foopkg,因为在Go语言中,包的名称就是在.go文件顶部的package关键字后面的内容,只要它是一个有效的标识符。

目录的唯一用途是收集具有相同包名的文件集合。这在规范中也有强调:

> 具有相同PackageName的一组文件构成了一个包的实现。实现可能要求一个包的所有源文件都位于同一个目录中。

在Go语言中,约定是目录与包名匹配,但这并非必须如此,而且通常在第三方包中并不是这样。标准库在遵循这个约定方面做得很好。

现在,目录的作用在于导入路径。你可以在单个二进制文件中有两个名为'foo'的包,只要它们具有不同的导入路径,例如:

/some/path/1/foo/some/path/2/foo

我们还可以非常高级地给导入路径设置别名,例如:

import (
    bar "/some/path/1/foo"
    baz "/some/path/2/foo"
)

再次强调,这样做的原因不是因为包名必须是唯一的,而是包的导入路径必须是唯一的。

从这个陈述中可以得出另一个见解--在一个目录中,你不能有两个包名。Go编译器会抛出一个错误,指出它无法加载包,并且找到了包foo(foo.go)和bar(bar.go)

请参阅https://golang.org/doc/code.html#PackageNames获取更多信息。

英文:

If what you said was true, then your function call would actually be 1-foodir.FooFunc() instead of foopkg.FooFunc(). Instead, go sees the package name in 2-foofile.go and imports it as foopkg because in go the name of the package is exactly what comes after the words package at the top of .go files, provided it is a valid identifier.

The only use of the directory is for collecting a set of files that share the same package name. This is reiterated in the spec

> A set of files sharing the same PackageName form the implementation of a package. An implementation may require that all source files for a package inhabit the same directory.

In go, it is convention that the directory match the package name but this doesn't have to be the case, and often it is not with 3rd party packages. The stdlib does do a good job of sticking to this convention.

Now where directories do come into play is the import path. You could have 2 packages named 'foo' in your single binary as long as they had different import paths, i.e.

/some/path/1/foo and /some/path/2/foo

And we can get really swanky and alias the imports to whatever we wanted, for example I could do

import (
    bar "/some/path/1/foo"
    baz "/some/path/2/foo"
)

Again the reason this works is not because the package name has to be unique, but the package import path must be unique.

Another bit of insight to glean from this statement is -- within a directory, you cannot have two package names. The go compiler will throw an error stating it cannot load package and that it found packages foo (foo.go) and bar (bar.go).

See https://golang.org/doc/code.html#PackageNames for more information.

答案2

得分: 10

大致解释一下为什么:

  1. 包有一个“名称”,由包声明中的package thepackagename设置在源代码的开头。

  2. 导入包是通过相当不透明的字符串完成的:在导入声明中的导入路径。

第一个是名称,第二个是如何找到该名称。第一个是给程序员的,第二个是给编译器/工具链的。
对于编译器和程序员来说,这种方式非常方便,可以声明:
> 请导入位于“/some/hierarchical/location”的包

然后在语句中使用其简单名称robot来引用该包,例如:

robot.MoveTo(3,7)

请注意,像这样使用包:

/some/hierarchical/location.MoveTo(3.7)

不是合法的代码,也不易读、不清晰、不方便。
但是对于编译器/工具链来说,如果导入路径具有结构并允许表示任意包位置,这是很好的,即不仅限于文件系统中的位置,还可以在存档文件、远程机器等中。

还要注意的是:有Go编译器和go工具。Go编译器和go工具是不同的东西,go工具对于如何布置代码、工作空间和包有比Go编译器和语言规范要求更多的限制。(例如,Go编译器允许将来自不同目录的文件编译为一个包而没有任何问题。)

go工具要求一个包的所有(有特殊情况,我知道)源文件都位于一个文件系统目录中,并且常识要求该目录应该“命名为包名”。

英文:

Roughly for the why:

  1. Packages have a "name" which is set by the package clause, the package thepackagename at the start of your source code.

  2. Importing packages happens by pretty much opaque strings: the import path in the import declarations.

The first is the name and the second how to find that name. The first is for the programmer, the second for the compiler / the toolchain.
It is very convenient (for compilers and programmers) to state
> Please import the package found in "/some/hierarchical/location"

and then refer to that package by it's simple name like robot in statements like

robot.MoveTo(3,7)

Note that using this package like

/some/hierarchical/location.MoveTo(3.7)

would not be legal code and neither readable nor clear nor convenient.
But to for the compiler / the toolchain it is nice if the import path has structure and allows to express arbitrary package locations, i.e. not only locations in a filesystem, but e.g. inside an archive or on remote machines, or or or.

Also important to note in all this: There is the Go compiler and the go tool. The Go compiler and the go tool are different things and the go tool imposes more restrictions on how you lay out your code, your workspace and your packages than what the Go compiler and the language spec would require. (E.g. the Go compiler allows to compile files from different directories into one package without any problems.)

The go tool mandates that all (there are special cases, I know) your source files of a package reside in one file system directory and common sense mandates that this directory should be "named like the package".

答案3

得分: 7

首先,包声明和导入路径是不同的概念。

包声明 声明了 PackageName

PackageClause  = "package" PackageName .
PackageName    = identifier .

包声明的目的是将文件分组:

具有相同 PackageName 的一组文件形成一个包的实现。

按照约定ImportPath(见下文)的路径基名(目录名)与 PackageName 相同。为了方便起见,建议你不需要考虑要使用什么 PackageName

然而,它们可以是不同的。

基名只影响 ImportPath,请参考导入声明的规范

ImportDecl       = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) .
ImportSpec       = [ "." | PackageName ] ImportPath .
ImportPath       = string_lit .

如果省略了 PackageName,则默认为导入包的包声明中指定的标识符。

例如,如果你有一个名为 foo 的目录,但在其中的一个源文件中声明了 package bar,当你导入 <prefix>/foo 时,你将使用 bar 作为前缀来引用该包中的任何导出符号。

darehas 的回答 提出了一个很好的观点:你不能在相同的基名下声明多个包。然而,根据包声明,你可以将同一个包分布在不同的基名下:

实现可能要求一个包的所有源文件位于同一个目录中。

英文:

First thing first, the package clause and import path are different things.

package clause declares PackageName:

PackageClause  = &quot;package&quot; PackageName .
PackageName    = identifier .

The purpose of a package clause is to group files:

> A set of files sharing the same PackageName form the implementation of a package.

By convention, the path basename (directory name) of ImportPath (see below) is the same as PackageName. It's recommended for convenient purposes that you don't need to ponder what's the PackageName to be used.

However, they can be different.

The basename only affects the ImportPath, check spec for import declartions:

ImportDecl       = &quot;import&quot; ( ImportSpec | &quot;(&quot; { ImportSpec &quot;;&quot; } &quot;)&quot; ) .
ImportSpec       = [ &quot;.&quot; | PackageName ] ImportPath .
ImportPath       = string_lit .

> If the PackageName is omitted, it defaults to the identifier specified in the package clause of the imported package.

For example, if you have a dir foo but you declare package bar in a source file resides in it, when you import &lt;prefix&gt;/foo, you will use bar as a prefix to reference any exported symbols from that package.

darehas' answer raises a good point: you can't declare multiple packages under the same basename. However, according to package clause, you can spread the same package over different basenames:

> An implementation may require that all source files for a package inhabit the same directory.

答案4

得分: 0


规则

  • package xxx 声明包名,这是导入时的默认别名。
  • import path/to/dir 使用目录路径,不一定是包名。
  • 两个不同的目录可以有相同的 package xxx
    • 但是当在同一个 Go 文件中导入这两个目录时,需要给它们不同的别名。
    • 这两个目录甚至可以相互导入和使用。
  • 如果使用 package __(双下划线),那么:
    • 导入这样的包时,必须指定别名,不能省略,否则找不到该包。
    • 导入时,编辑器(例如 Goland)无法自动搜索,必须手动导入。

示例代码

(下面的文件的共同基目录是 eric/go/pkg_learn/pkg_dir_diff

源文件树:

eric/go/pkg_learn/pkg_dir_diff
├── d
│   └── d.go
├── d1
│   └── d1.go
├── d2
│   └── d2.go
├── pkg_1_and_2_test.go
├── pkg_1_test.go
└── pkg_2_test.go
  • eric/go/pkg_learn/pkg_dir_diff/d/d.go

    package __
    
    func D() {
        println("d")
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/d1/d1.go

    package d
    
    import d2 "eric/go/pkg_learn/pkg_dir_diff/d2"
    
    func One() {
        println("one")
        d2.Two()
    }
    
    func OneTwo() {
        One()
        d2.Two()
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/d2/d2.go

    package d
    
    func Two() {
        println("two")
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/pkg_1_test.go

    package pkg_learn
    
    import (
        d "eric/go/pkg_learn/pkg_dir_diff/d1"
        "testing"
    )
    
    func TestSamePkgFromDifferentDirs1(t *testing.T) {
        d.One()
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/pkg_2_test.go

    package pkg_learn
    
    import (
        d "eric/go/pkg_learn/pkg_dir_diff/d2"
        "testing"
    )
    
    func TestSamePkgFromDifferentDirs2(t *testing.T) {
        d.Two()
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/pkg_1_and_2_test.go

    package pkg_learn
    
    import (
        d "eric/go/pkg_learn/pkg_dir_diff/d"
        d1 "eric/go/pkg_learn/pkg_dir_diff/d1"
        d2 "eric/go/pkg_learn/pkg_dir_diff/d2"
        "testing"
    )
    
    func TestSamePkgFromDifferentDirs1and2(t *testing.T) {
        d1.One()
        d2.Two()
        d1.OneTwo()
    }
    
    func TestD(t *testing.T) {
        d.D()
    }
    
英文:

Rules

  • package xxx declare the package name, which is the default alias when import.
  • import path/to/dir will use the dir path, not necessarily package name.
  • 2 different dir can have the same package xxx.
    • But when import the 2 dir in the same go file, need to give them different aliases.
    • The 2 dir can even import & use each other.
  • If u use package __ (double underscore), then:
    • When import such package, must specify alias, can't omit, otherwise can't find the package.
    • When import, editor (e.g goland) can't auto search, must import by hand.

Example code

(The common base dir for files below is eric/go/pkg_learn/pkg_dir_diff)

Source file tree:

eric/go/pkg_learn/pkg_dir_diff
├── d
│&#160;&#160; └── d.go
├── d1
│&#160;&#160; └── d1.go
├── d2
│&#160;&#160; └── d2.go
├── pkg_1_and_2_test.go
├── pkg_1_test.go
└── pkg_2_test.go
  • eric/go/pkg_learn/pkg_dir_diff/d/d.go

    package __
    
    func D() {
        println(&quot;d&quot;)
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/d1/d1.go

    package d
    
    import d2 &quot;eric/go/pkg_learn/pkg_dir_diff/d2&quot;
    
    func One() {
        println(&quot;one&quot;)
        d2.Two()
    }
    
    func OneTwo() {
        One()
        d2.Two()
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/d2/d2.go

    package d
    
    func Two() {
        println(&quot;two&quot;)
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/pkg_1_test.go

    package pkg_learn
    
    import (
        d &quot;eric/go/pkg_learn/pkg_dir_diff/d1&quot;
        &quot;testing&quot;
    )
    
    func TestSamePkgFromDifferentDirs1(t *testing.T) {
        d.One()
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/pkg_2_test.go

    package pkg_learn
    
    import (
        d &quot;eric/go/pkg_learn/pkg_dir_diff/d2&quot;
        &quot;testing&quot;
    )
    
    func TestSamePkgFromDifferentDirs2(t *testing.T) {
        d.Two()
    }
    
  • eric/go/pkg_learn/pkg_dir_diff/pkg_1_and_2_test.go

    package pkg_learn
    
    import (
        d &quot;eric/go/pkg_learn/pkg_dir_diff/d&quot;
        d1 &quot;eric/go/pkg_learn/pkg_dir_diff/d1&quot;
        d2 &quot;eric/go/pkg_learn/pkg_dir_diff/d2&quot;
        &quot;testing&quot;
    )
    
    func TestSamePkgFromDifferentDirs1and2(t *testing.T) {
        d1.One()
        d2.Two()
        d1.OneTwo()
    }
    
    func TestD(t *testing.T) {
        d.D()
    }
    
    

huangapple
  • 本文由 发表于 2017年4月24日 12:29:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/43579838.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定