-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: make imported symbols predictable #29036
Comments
I think the cognitive overhead is a very important point. Code will be read many more times than written, and Go is already optimised to be easy to read before being easy to write. For example, most of the points I've seen in favor of dot imports is that they simplify writing quick programs, or swapping imports without needing to rewrite much of the program. I think the solution to that should be tooling, not adding complexity to the language. |
I like this, as it codifies existing best practices. In general, I'd prefer to derive the identifier from the import-path (i.e. I don't like the "require an import symbol for every import" solution). There is one complication, in that I still think that the identifier should - by default - be the package name. But the package name is specified in the importee, and there is no way to tell from a package alone what name it's imported as. So maybe this change should also talk about canonical import paths - maybe making them required instead of optional? Maybe getting rid of the "package name" and instead have the package-clause refer to the canonical import path? Just spit-balling. But I do think this proposal only talks about half of the equation and should say something about the role of package names and the importee too. |
Oh, as a corollary, BTW: This could also be a tooling-enforced solution (to address the "the spec doesn't care about import-paths" part) in that the Go tool could refuse to work if a package name doesn't follow the requirements set out - exactly like we do with canonical import paths, where it's the go tool that will refuse to work if you choose the wrong import path. |
That's why the proposal says "when lacking an explicit import package name, the package name must match a name derived from the import path". If the package name declared in the package is different (and there's no local name specified in the import), it would be an error. So it's entirely possible to have a package with name |
Ah, sorry, read over that.
Hm, IMO that's not great. It means goimports at least would still need to walk/parse (at least in part) the entire workspace to find an import. Which AIUI is one of the main reasons goimports (and by extension, saving, if you run it as a hook) is slow. If we're touching the rules relating import paths to package names, it seems like a good idea to just go ahead and codify the best practice of "last component of import path == package name" once and for all. But either way, I think this is a good idea :) |
I think that's going a step too far. It would mean I can't copy a package verbatim to another location and use it without changing the package name it declares. Also, what about versioned import paths? The rule as proposed would allow you to import "foo/bar/v2" as |
I've found dot-imports to be useful when writing tests. For instance:
I wouldn't really mind the dot-import being removed. I was just adding my particular use case for them. |
What about things like I really like this proposal, just making sure everything is covered. Would |
I don't have strong feelings on the proposal itself but I can add some commentary about goimports, since I've been working on it a lot lately and plan to make changes in this area soon. Per #28428, goimports is going to change to add an explicit name to any import of a package whose doesn't "obviously" match its import path. The algorithm I'm using is the same one Bryan proposed above. And yes, this means that With that done, goimports won't need to scan the workspace for a file whose imports are satisfied. It's not perfect, because without this proposal, nothing prevents someone from doing:
which will confuse goimports into thinking everything's fine. (The only way to avoid the confusion would be to load everything even if the file looks okay, which is exactly what we don't want.) But that's very contrived. |
@deanveloper > I've found dot-imports to be useful when writing tests. Dot import might still be allowed in |
This has been a source of complexity in almost every project I've worked on. In fact my most recent project needs a significant amount of documentation to explain, and alternative implementations for different environments: https://github.com/dave/dst#resolvers It would be trivial for I'm perhaps guilty of over-using dot-imports, but I certainly wouldn't mind if they were removed. |
One thing that might be done is put this restriction in vet (which is now run by go test) instead of the language. |
Note that the exact rule proposed here doesn't seem to work with modules, in which programs can be expected to have imports like
|
While that's possible, we would miss an important advantage. Adding the restrictions to the language, tools and libraries like If we only enforce these restrictions in I realise that changing the language can be tricky, though. Perhaps enforcement in vet could be a first step. But in my mind it would be great if it were impossible to write Go programs using confusing imports in the future. |
Yes, that's true. But the alternative is probably to bake some kind of understanding of major versions into the language spec, which doesn't seem quite right to me. Note that goimports will already rewrite the above as:
so ISTM that it's not necessarily a problem to require that in this case. |
I think that if modules catches on we should expect to see more and more "/vN" package paths over time, so we should plan for that up front. It doesn't seem desirable to wind up requiring a local import alias for the majority of imports. |
There's a problem with that, unfortunately. It is legitimate to name a package "vN" (for some number N) so if you don't require an explicit identifier in that case, you can't tell the difference between a major version import (which should use the previous path element) and a package that is in fact named vN. I guess the assumption could then be the other way - if you really do have a package with a name that matches |
@heschik You're right. It's behaving as we'd want it to if this proposal was accepted. |
I really like this idea, though I think I would tweak the wording slightly.
I don't think that heuristic is workable. then I would instead say that it should use the prefix from the last part of the path including all characters that are valid in go identifiers. I don't think length should matter. thus |
I would worry we're trying to be too clever here. I agree we should be aware of major versions, but I would think if the next part of the path isn't a valid identifier, we should just require an alias. Remember this is probably just for historical packages... I find it rather unlikely that after this change many people will be creating packages that have names that require an alias... |
This proposal has two separate issues--banning dot imports and using predictable import names--and I think they are separable. I moved banning dot imports into #29326. We can keep this issue for discussing predictable import names. |
Note that if we adopted this proposal, the package clause would be nearly meaningless. The main thing the package clause does today is set the name of the package, but with this proposal in effect every import would have a local import alias. See #21479. |
The tool you describe is complex, to put it mildly. Part of the reason that the Go tooling ecosystem is rich and interesting is that the barrier to entry is pretty low. |
I think there is some misunderstanding here (and it may be mine) The only added restriction would be that the current behavior of goimports (adding a local alias that matches the package clause if it is not derivable from the import path) is required behavior, and can be relied on. The extra work would be codifying the set of rules that goimports uses to decide if the package name is derivable, making them available as a library for tools to use, and adding them to a vet check in the compiler as well, to reject code that does not match that standard. |
Not really. The "simple" tools like the guru are complex, because they are monolithic. And this issue is the product of this complexity. |
@ianthehat's remarks seem on point to me. For the record, I scanned through Russ's Go corpus and checked which package names didn't match their package paths (excluding main packages) using this algorithm as used by goimports:
Just under 10% (462/5092) of the packages resulted in a mismatch. About 2.5% (1560/62791) files used such an import without using an explicit identifier; this includes about 14% (830/5875) of all packages over 25% (138/553) of all repositories. This does seem like more than I was expecting, but as the change could be entirely automatic, with backward compatibility maintained for packages that declare earlier Go versions, I don't think the requirement would be that onerous. I think the net change would be to make the code base considerably more readable. |
To respond to a couple of @josharian's points:
You can use whatever import path you like, but if there's a mismatch with the package name, people will be required to use an explicit name when importing the package. This seems good to me. If I see an import of
Tools would be able to assume it if the correct Go version is declared in the go.mod file.
I don't think this should have similar implications, because old tools can understand the new code just fine. That is, an new program read by an old tool will have exactly the same semantics as it does currently. The set of programs accepted by the new version of the compiler would just be somewhat smaller than it is today. |
A bit late to reply to @josharian's comment, and some of what I wanted to say has already been said by others. Just adding a few bits.
Agreed. This is where I expect people with much more experience in the language and its tooling to step in :)
Or even simpler - after a few Go releases, tools could simply error if a program doesn't follow the new import rules. They could just ask the user to run goimports on the packages and try again.
Right, the point is to increase the amount of pain for those who violate the rules :) Everything should still work, modulo asking the developer to re-run goimports if they want to use newer tools, much like @ianthehat mentioned earlier. But most importantly, we'd drastically reduce the amount of pain that tool developers have to put up with. |
It wouldn't surprise me in the slightest if the misunderstanding is mine. :) Thanks all for bearing with me.
Just running goimports will fix existing code to work. However, this proposal increases pressure on package authors to rename their packages to be in conformance with preferred naming, which will be breaking changes, and ones that aren't as easy as "run goimports" to deal with. That will be less painful with dependency management, but it will still be painful. @rogpeppe thanks for providing actual code! Note that that code can generate invalid package names. For example, given import path "commaok.xyz", it returns import name "commaok.xyz" ( https://play.golang.com/p/LLiPkqqsTXZ) but import names can't have dots in them. Maybe it's enough of a corner case that it's ok, but it is a bit awkward. As I said before, I think trying to nail down exactly what the path -> name rule is is the next important step for this proposal. And see whether there are other heuristics we could use to get that 10% number down. And nail down corner cases like v8 and commaok.xyz.
Just to make sure we're 100% on the same page, when you say "derivable", you mean that given a package path there is a single, unique, correct package name (not a set of acceptable names)? |
Just to be clear on the case of v8: #28435 (comment) and in the worst case #28435 (comment). |
Ok, let's say
Any tool or developer can look the file to get required information. And, seems the
|
Have to throw in another "strongly opposed" on this one, due to the proposed prohibition on dot imports. While they can be overused, there are definite situations where they make code substantially cleaner, usually when using "expressive" type packages. A good example is the popular BDD framework pair Ginkgo + Gomega. This:
is substantially more readable and expressive than:
And that's a fairly basic construction of Gomega matchers and assertions. I see no reason to ban this functionality just because some people might misuse dot imports in other circumstances. |
As someone that's used a not-dissimilar package (gopkg.in/check.v1) for a long time, I'd have to respectfully disagree. Originally, we imported that package to dot for similar reasons, but we moved to using a short alias ( For example, I am not at all familiar with the gomega package. If I am trying to understand some test code that imports gomega to dot, I have to be aware of all the 64 exported symbols that the gomega package exports. By the way, your code fragment above isn't quite right. You don't need to package-qualify the
That's actually not an insignificant thing - use of methods and fields is often much more common than use of top-level identifiers. I'd suggest that using a short import alias could work OK for you; for example:
or even:
If I see that code, it is immediately clear to me that Expect and Equal come from somewhere external and where. I believe that property is worth a little more weight on the page. |
I strongly support this proposal, especially regarding removing dot imports. Whatever readability the feature may provide in specific circumstances is insignificant compared to the ambiguity it creates in the whole. My experience in training and support has also shown that it's one of the most frequently mis-used (i.e. over-used) features by new Go programmers. |
As a tool or sdk creator I create the same tool for multiple languages and differentiate them in different repos using a prefix (or maybe suffix). For example:
I would expect my import to remain |
Isn't this something a linter could solve? Is there really a need for language change here? But yea, just recently I have made code with the following imports: import (
"github.com/deckarep/golang-set"
"github.com/go-git/go-git/v5"
"github.com/whilp/git-urls"
"github.com/xanzy/go-gitlab"
"github.com/xmidt-org/gokeepachangelog"
) Good luck guessing how they are imported. But I think a linter which would require one to specify an alias in such cases, matching the package name, would solve most of the problems: import (
mapset "github.com/deckarep/golang-set"
git "github.com/go-git/go-git/v5"
giturls "github.com/whilp/git-urls"
gitlab "github.com/xanzy/go-gitlab"
changelog "github.com/xmidt-org/gokeepachangelog"
) |
@mitar optional tools such as goimports have enforced this for some time. The problem is that, as long as the toolchain and language don't enforce predictable imports, then all the tools and libraries must support both forms. Hence the proposal to restrict this in the language itself. |
Dot imports is about code style, not about the language. |
I do not find any option to enforce this with goimports. Maybe some other tool? |
Oh, true. I didn't notice it working because it has some exceptions, so it didn't do anything for |
Currently when a package is imported, it's not possible to tell what package name it uses without going to the package itself to see what name it uses.
This can make it slow to analyse code (for example to jump to a symbol definition). For example, say we want to find the definition of the symbol
X
in the expressionm.X
. If m isn't defined in the local package, it's necessary to obtain a copy of every imported package's code to see if it's defined by any of them. For large packages, that can take a long time (many seconds). Even with caching proxies, it can still take a long time if the set of dependencies has recently changed.There's also the issue of cognitive overhead: just seeing an import statement is not sufficient to know what symbols that import statement brings into scope. For large programs, that overhead can be significant, and is worse when one or more of the dependencies are no longer available - the reader of the code is forced to guess what symbols are imported.
This issue is even worse when "." imports are used to bring all a package's exported symbols into local scope. Even though dot-imports are frowned upon, there is still no universal consensus that they should be avoided.
The goimports tool already implicitly acknowledges this issue by adding an explicit package name when the symbol isn't clear from the import path. Although this helps, this doesn't help in the general case, because tools cannot assume that goimports has been run.
I propose the following:
This would mean that we can always look at any symbol in a piece of Go source code with local context only and know definitively whether it is defined in the local package or not, and if not, exactly which import path would need to be investigated to find the symbol.
Dot imports
The official guidelines suggest that a dot import should only be used "to let the file pretend to be part of package foo even though it is not". This is a stylistic choice and strictly unnecessary. The tests can still use package-qualified names with only minor inconvenience (the same inconvenience that any external user will see).
Other than that, I believe the most common use is to make the imported package feel "close to a DSL". This seems to be actively opposed to the aims of Go, as it makes the programs much harder to read.
Predictable imported symbols for imports
The simplest possibility here would be to require an import symbol for every import path. As many (most?) people use goimports to manage their import statements, this might not be too onerous a requirement.
Another approach would be to state that when lacking an explicit import package name, the package name must match a name derived from the import path. Possible rules might be:
This means that we could carry on importing
"fmt"
without redundantly specifyingfmt "fmt"
, but has the disadvantage that nothing else in the language specification talks about what's inside an import path.The text was updated successfully, but these errors were encountered: