> Go are all able to (in most cases) handle conflicting upstream dependencies. In other words, if package A required version 2 of C, and B requires version 1, the code can still compile without any user intervention (such as shading) barring some corner cases.Different languages place different constraints on package management. For Go:
1. The language is structurally typed. If A gets a value from C 2.0 and passes it to B which expects a C 1.0 value, as long as the interfaces are structurally compatible, it will compile. (Whether it runs correctly is a completely open question. In principle, it won't because C is explicitly saying that 1.0 and 2.0 are incompatible. But there's nothing in the language to prevent a value from one version of C being passed to a function from the other version of C.)
Dart (like most languages) is nominally typed. If you have a class named Foo defined in two different libraries (or different versions of the "same" library), they are considered different, unrelated classes. If Dart were to allow multiple versions of the same package, then users will run into compile errors like "Expected a Foo (1.0) here but got a Foo (2.0)."
2. Code size is relatively less important for a primarily server-side language. Most Go developers don't really care how big their executable is as long as its within reason. Want to compile in five versions of some package? No big deal.
Dart is designed for client-side mobile apps, both native and compiled to JS. That means small code size is an absolutely critical performance requirement. Silently allowing multiple versions of the same package even when it's possible to find a single version that satisfies all constraints would bloat applications for no benefit.
This was historically a problem when people started trying to use the Node/NPM ecosystem in the browser. NPM freely gives you multiple versions of packages and the end result was large apps that weren't friendly to running in the browser. NPM added shared dependencies later to try to mitigate this, but now you've got two ways to manage dependencies and the extra complexity that entails.
3. Go chose to bake the major version number directly into each import in the source files. This makes imports unambiguous in the presence of multiple major versions of a package. But it means that upgrading a package to a new major version is a sweeping transitive source code change even when the new major version is in fact backwards compatible, which is the common case.
For Dart, we wanted users to be able to rev versions more easily than that and keep version management outside of source code.
> Dart's authors decided to use the traditional Java style package management algorithm (which in the worst case, on paper at least, requires NP time constraint solving but this is rarely an issue in practice).
Pub is most closely based on Bundler (which in turn informed the design of Cargo), and not Maven/Ivy/Ant. You're correct that version constraint solving is NP-complete. Fortunately, we have a state of the art solver [1] and it generally finds solutions very quickly. When it fails, it tends to fail fast and report helpful errors.
It's not perfect but there is no silver bullet for code reuse, and package management is fundamentally about code reuse. Code reuse is hard and anyone claiming they have a solution that makes it easy is most likely sweeping some unsolved part of the problem under the rug.
[1]: https://nex3.medium.com/pubgrub-2fb6470504f