Go’s major versioning sucks – From a fanboy
wagslane.dev
wagslane.dev
Maybe on a small scale breaking changes don't cause issues, but in large-scale development, you simply can't afford to have a bunch of random CI/CD pipelines fail because a developer decided to "accommodate for" a backwards-incompatible change without separating it to a new import path.
>Using different paths for different major versions makes more sense in situations where we may require two different versions of the same package, you know, diamond imports and all that. This is the exception, not the rule, and it seems strange to tap dance around a problem that doesn’t exist in most codebases.
Diamond imports are not as rare as you think - it's just that you never run into it in your own code, but in the code of your dependencies. And in Go, the problem is already solved for you so you never even see the solution at work.
Let's say you use two libraries for, say, backend and frontend - libbackend and libfrontend. Let's say they both depend on some parser library, say libparser, version 1.0.
Now assume that libbackend upgrades the libparser dependency to 2.0, but the import path stays the same and your application - the one that has been successfully auto-updated on a CI/CD server for years and nobody remembers how it works anymore - breaks, and you have no idea why, compiler reports some weird API-incompatibility errors, and now you have to spend two weeks getting back into the codebase and debugging the issue, until you realize you can't really fix it, because the guy developing the language decided that diamond imports are "a problem that doesn’t exist in most codebases".
That said, I think people greatly undersell how well Go has done packaging. There’s, again, tradeoffs that are at least arguable; but, I hate when people say things like “Didn’t (npm|apt|cargo|…) already solve this?” Honestly, often times the answer is at best “kind of?” — a lot of these packaging and versioning problems remain without complete solutions. Meanwhile, Go has some novel design choices that set it apart. The module proxy is a slightly unfortunate tradeoff, but Go remains one of the only languages with a good story for mostly decentralized package management. The module proxy is more of a hack that gets you some of the advantages of centralization without strictly depending on it. But beneath that, it’s nice that you can pull packages from basically anywhere with several VCSes.
That way if a dependency does a minor upgrade that breaks your code but you also need some new functionality, you could depend on the old code in a particular package and new code in another.
kardianos commented on May 23, 2018:
One goal of vgo is to not make the source go files dependent on the module file. If the vgo file is removed, the worset result is a fresh build gets the latest in major version series.
This proposal appears to break that principal.
Source: https://github.com/golang/go/issues/25518#issuecomment-39134...Prior to go modules this was even worse with third party packages. I’ve had cases where my app has integrations with a few google services like maps, sheets, and text-to-speech. It turns out the libraries for all of these services, understandably, also have certain dependencies in common. If you run in to the unfortunate situation where you need to update one of the google libraries, and that updated library now relies on an updated version of a dependency required by others, you’ll end up needing to update all of the other libraries. Granted it’s usually a good idea to update, but it turns a theoretical 1 hour task (update library to use new feature) in to a headache that might take several days depending on how big the changes were to the other libraries
Well, you will anyway have to spend a few days, often. You either spend it taking a new version of several libraries, or you spend it digging through bizarre issues because your upgrade of maps broke your integration with sheets, since your older sheets was never tested with the newer shared library version and has a bug.
The same scenario is very likely to happen, but much worse, if libparser was upgraded from 1.1.1 in 1.2.3 for libbackend. This will mean libfrontend, in your app, now also uses libparser 1.2.3. But, libparser 1.2.3 has a bug that affects libfrontend, but not libbackend. But of course your CI/CD won't tell you that, because Go is "smart" (and so are NPM, Cargo, etc) and instead of letting you know you have a depe dependency mismatch and can't upgrade libbbackend, they let you have fun debugging a runtime issue. Odds are good you will spend a good part of the day before even realizing that libfrontend, which says right there that it uses libparser 1.1.1, is actually being compiled with a different version because something else changed.
The moral of the story: SemVer is a lie. If you want a long standing project to keep running, you pin specific dependency versions for every library, and always allocate a good chunk of time for any upgrade. "Automatic upgrades" of dependencies are a nice dream, just like code that has no bugs.
You are quite flexible in specifying the versions of your direct dependencies and you can fix major, minor or patch versions or do even more complex stuff with version ranges (even though I see the latter rarely used in practice).
In addition, different versions of the same crate can coexist (but their types are considered distinct). So if I directly use libparser 1.1.1 but one of my dependencies uses libparser 1.2.3 that's totally fine. The version of my direct dependency won't silently be upgraded (assuming you fixed the minor or even the patch version when specifying that dependency).
Then just use 0.y.z versions and be done with it?
If the library constantly changes and everybody expects that, then that seems fitting.
I like Go's major version handling very much. If it's backwards incompatible, it's basically a new library under the same name and development team.
In my opinion making major version updates so painful also incentivizes not making backwards incompatible changes in libraries, which results in the Go ecosystem being very stable overall (something I value a lot in my day-to-day).
This, this, and a hundred more times this.
Incompatible is incompatible. There is no "kinda incompatible", "99% compatible" - when it comes to dependencies, they either work properly or they don't.
Software should be eternal. Without being strict about semantic versioning, it is impossible to make it so.
An HTTP server can remain API compatible, and still drop or break support for a major feature you care about. Surely you don't want that to ship in a patch release.
Adding a new struct field, even a private one, can break API compatibility in Go if people use your struct without named fields. Do you want a new library in this case or not?
Semver doesn't cover CLI compatibility, either, do you want a CLI redesign to remain v1 or become v2?
Nuance matters. Stating to "just be strict about semantic versioning" doesn't help, semver is fuzzy.
If a change results in user programs breaking, it's a bug in the
kernel. We never EVER blame the user programs.Even in the Linux example, kernel modules do not receive compatibility guarantees because it is difficult to keep. You may need to rewrite your module depending on how it is written when upgrading the kernel. It also doesn't apply in case of security vulnerabilities and certain classes of bugs. Technically viruses can be user programs which rely on those vulnerabilities and even excluding those, there are cases where some API seems benign but later turns out to be flawed (like precise timers in the context of browsers).
I define it as "provides the equivalent API".
Other than that, I don't understand your question. What does "use the same method signature but with different meaning" mean? What is my "duck typing code"? Is duck typing part of your API?
You may say "Well, don't do that," but it seems like golang users think of this as a way to provide backwards-compatible API upgrades[0].
> What does "use the same method signature but with different meaning" mean?
.draw(times: int)
In your code that adds a shape to the canvas. In my code it adds a card to your hand.> What is my "duck typing code"?
You collect everything with the draw signature as options to draw with.
> Is duck typing part of your API?
What do you mean “your API?”. Maybe I mention it in the docs, maybe I don’t. But that doesn’t really matter because if we’re going to compare intent, then this is subjective. And it really doesn’t matter because duck typing isn’t part of an API, it’s a technique / language feature. By just adding a method, I removed your ability to do something.
My point with all this is your definition is lacking. You just shifted the pain of defining “breaking change” to the pain of defining “compatible API”. Which is fine if you’re then going to define that strictly (see Hickey’s talk on SemVer), but you haven’t.
1) Machine-readable definitions of the functions and data types used
2) Human-readable explanation of what the previously defined functions do when passed each of the allowed data types, with a high-level explanation of how is the API supposed to be used.
The first part is usually implemented as part of your programming language - in case of Python, type hints can be used as a substitution.
The second part is necessarily informal, since it targets humans. But that does not mean it cannot be well-defined. For example:
mmap() creates a new mapping in the virtual address space of the calling process. The starting address for the new mapping is specified in addr. The length argument specifies the length of the mapping (which must be greater than 0).
There is no concept of "virtual address space" or "memory mapping" in C. All C knows about is functions and values. But we still know what mmap() does and there is absolutely no ambiguity to its behavior.It's simple: a user inherits from your class and adds a method to their user-defined subclass. You then ship a new version of your library that adds a method with the same name to your class, but with different semantics, and other parts of your library call this method expecting it to have the semantics you defined. You've just broken the user's code, even though you say this was a "backwards-compatible" release, and it isn't the user's fault because they were using your library as it was documented.
https://stackoverflow.com/questions/1456785/a-definitive-gui...
That's why I prefer ComVer: https://gitlab.com/staltz/comver
For example, assume that you're using a dependency, libparser, v1.0, which uses ComVer. In version v1.1 they add a new function, foo() which does something that you want. However, at the time of you writing the software, the latest version was v1.2, which you use because you're a good engineer who likes to keep things up-to-date.
Now think about what happens when you distribute your application to three people:
- person 1 has libparser v1.0 - you program can't work, because v1.0 doesn't have foo(), so you obviously must require minor version that your program was built with - which is v1.2
- person 2 has libparser v1.2 or higher - you program works.
- person 3 has libparser v1.1 - you program CAN work, since v1.1 contains foo(), but it WILL NOT work, because your application requires at least v1.2.
Now, you might say, "well a developer should know that foo() was added in v1.1 and pin that as the lowest required vesion" - in which case I reply that ComVer doesn't help you a tiniest bit with this, because both API extensions and bugfixes are mashed together into a minor version, so now you have an order of magnitude more changelogs to go through.
SemVer stricly separates API extensions and bugfixes so you can just browse the minor version changelogs and see which version added the feature you require.
If you want your software to be pristine forever, you really need to pin your dependencies, ideally via copying the source of the library into your repo so you aren't reliant on a package manager being available in the future. For library developers, regardless of versioning scheme you need to avoid ANY breaking changes whatsoever. Instead of changing an existing function, introduce a new one so that your downstream users can still access the old behavior while keeping up to date. Trust me when I say this will be much easier most of the time than patching old versions with security updates and bug fixes.
At least for typed languages where an incompatibility clearly caught at compile time is usually preferable over a murky "technically it's still the same API, but..." I'd rather have separate numbers for major incompatibility/minor incompatibility.
If you don't want to freeze your API, then just use 0.X.Y, everyone will understand that they need to do regular maintenance if they want to use your code.
But please, I beg you, do not use version 1+ if you're not planning to keep the compatibility promise.
I can change one part of an API and leave another untouched - that's part compatible, part incompatible. It's only an issue if you used the changed part.
(If you think that first one always counts... what if the changed part is literally called YouMustNotUseThisFunctionOrYourCodeWillAlwaysBreak() ? It's clearly implied to not be part of your intended API, despite technically being part of it.)
I can add something to a type, in a way that's backwards compatible at compile time... but common reflection patterns might cause everyone's code to explode.
I can make a change that solves a bug that someone was accidentally relying on by doing the wrong thing, but doesn't affect compile-time behavior, nor runtime for anyone using the library the way they should. But that bug-user's code is now broken, is this an incompatible change?
There is even a "law" for that: https://www.hyrumslaw.com/
When you commit to a version 1, you assume that every user of the package is using every feature you provide through your official API. If you break any slightest piece of the API, you've broken compatibility. It might "kinda work" for many users, but it will almost surely cause significant pain for many others.
> I can make a change that solves a bug that someone was accidentally relying on by doing the wrong thing, but doesn't affect compile-time behavior, nor runtime for anyone using the library the way they should. But that bug-user's code is now broken, is this an incompatible change?
It is not an incompatible change, and it is the responsibility of the bug-user's code to fix the bug in his program.
Of course, when such a thing happens on a large-enough scale, the API developer sometimes cannot afford to "fix" the behavior and force countless users to fix their programs, so the quirk just becomes de-facto part of the API.
Semver doesn't force you to do anything. You can stay on v0 forever for all it cares, or you can bump a major version every day.
What semver doesn't allow you to do is lie to your users.
That is not how semver works. If you read the documentation [0], it says right there that the public API must be clearly defined but that can also happen through documentation and you only have to up the major version if you break that specified public API.
You can certainly consider it bad programming practice to expose something via the means of the language that is not defined as public API but it is not against semver to do so.
[0]: https://semver.org/
if hash(libpath) != predefinedHash {
log.Fatal("broken compatibility teehee")
}
In the context of semver, "API" means "public API". There is no other API but public API.If that is what you meant with "official API" then we are on the same page.
And that's not something to accept and work around, but a problem, one that Go's hard stance solves.
For downstream consumers that gives 2 options: get stuck on an old version silently sometimes, or deal with an occasional breakage during usual dependencies updates. If the old version is used for talking to some external service, you will break one day.
However, in case that two versions are so incompatible that both sides of communication channel need to update, then I think it's fair to place responsibility on the service writers to notify their users (through deprecation warnings, documentation, chat, etc.) of the change.
Edit: Apparently this is wrong! See replies below.
[1]: https://fasterthanli.me/articles/abstracting-away-correctnes...
I just looked at the Git history and this is plain false. It already looked that way when the big source tree move (src/pkg/ -> src/) was done in 2014. Tracing it back further (to before Go 1.0 times, when there wasn't even a builtin error interface yet and the function returned os.Error), ImportedLibraries was *never* implemented in debug/pe.
1. go 1.9 adding monotonic clock readings in a breaking way, i.e. this program changed output from 1.8 to 1.9: https://go.dev/play/p/Mi6cGCPd0rS
2. net/http started defaulting to http/2 for the same methods, an obviously breaking change
3. go1.17 switched to silently truncating a lot of query strings https://go.dev/play/p/azODBvkb-zK
4. Check out 'PreferServerCipherSuites' on tls.Config. https://pkg.go.dev/crypto/tls#Config ; of course that was a breaking change.
There's a few other things like this littered throughout the go stdlib, and I've personally hit far more breaking changes in Go's stdlib than the "go 1 compatibility promise" would have you expect.
That's not a breaking change, if you were comparing Times with just "==" before 1.9, your program was broken as well:
From the 1.8 source [1]:
// Equal reports whether t and u represent the same time instant.
// Two times can be equal even if they are in different locations.
// For example, 6:00 +0200 CEST and 4:00 UTC are Equal.
// Do not use == with Time values.
func (t Time) Equal(u Time) bool {
return t.sec == u.sec && t.nsec == u.nsec
}
[1]: https://cs.opensource.google/go/go/+/refs/tags/go1.8:src/tim...There are many cases where you know that your times are in the same timezone, or you don't mind different timezones comparing as different, and so '==' used to work correctly quite often.
My program was clearly not broken in 1.7, when I followed the documentation "Equal ignores location, == uses location", and things worked well.
My program became counter to their documentation in 1.8. My program broke in 1.9.
I don't see how that is not a breaking change.
It seems to me like a flaw in the language that "==" can compare unexported struct fields.
Looks like it's possible to disallow comparing structs with "==" at compile time: https://go.dev/play/p/BfM6sDxlTq9. Not ideal, but better than nothing I suppose.
But of course, then such structs cannot be used as map keys: https://go.dev/play/p/JXzqHPInJ-G.
> Without being strict about semantic versioning, it is impossible to make it so.
There is no semantic versioning if you're that strict. You have to update the major version on every single release, no matter the contents, if your idea of backwards compatibility is that absolute.
Let me give you a concrete example: I have a go package with this code:
var ErrBadThing = errors.New("somthing bad happened")
func DoThing() error { /* maybe return ErrBadThing */ }
I notice I had a typo in my error message, "somthing" when I wanted "something". I fix that typo. Is this a breaking change?The answer is "it depends"
err := mypkg.DoThing()
if errors.Is(err, mypkg.ErrBadThing) { /* will still work after change */ }
if strings.Contains(err.Error(), "somthing") { /* will no longer work after change */ }
So, is it a breaking change or not? I would argue "no, not breaking, if the user uses the API as expected, it does not break", but you may argue otherwise.If you argue that is a breaking change, well, what about adding a new method, which breaks reflection (like 'reflect.NumMethod()' returns one more, so if someone relied on indexing into your methods with reflection, you broke em!)? What about someone downstream applying a "patch" to your code before compiling it? Any change can break that.
The go authors have taken a much less strict approach. Go is still semantically "v1", but they've made a ton of breaking changes, from making `net/http` silently switch to http/2, to changing the semantics of various tls and security related functions, etc etc.
This is an inevitable consequence of Hyrum's Law[0]. It's been observed before but I find Hyrum's Law a very good and concise summary of the problem.
If your API only specifies that AN ERROR is returned, then it's a bug in the user code to rely on an implementation detail.
Yeah, that would work, except a lot of people read 0.x.y versions as "alpha quality". Regardless of the actual code quality.
And besides, people are right to understand 0.x is risky b.c you are not guaranteeing backwards compatibility.
> Then just use 0.y.z versions and be done with it?
FWIW, I also work in an organization that thinks of libraries this way, and we've found success and simplicity in versioning (for production) our Go libraries as 0.X.0 where X is just a monotonically increasing number generated by the build pipeline.
- we don’t need any kind of backwards compatibility, we just update everything.
if you don't care about backwards compatibility, then you can stay on v1 forever. Have you considered a monorepo? That would simplify updating packages and give you the behavior you want.
- For the client to update, it’s not a simple path change in go.mod
if a package moves from v1 to v2, there are breaking changes in either api or behavior. I think this implies more than a simple change to go.mod. This also allows importing both versions of a package if necessary.
So instead off focusing on those changes I have to first fix potential dozens of files for no reason at all.
You don't have to, if you don't want to upgrade to a new major version.
If you do want to upgrade to a major version, which means that there are breaking changes in a package's API or behavior - you sure as hell want to check the correctness of every single line of code written using that package. Since every file that uses that package must contain an import statement, the import statements are an easily greppable indicator of which files you have to check and potentially fix.
Yes, I do. Doing the monkey job of changing every import line (which will be done with a global search/replace) is definitely not that.
If you're relying on import statements to tell you which files to check, you're definitely doing something wrong
IDEs can even pinpoint those locations without building a project
2. You run tests and they fail if the behavior changed
Literally nowhere is "oh, do an automatic search/replace of imports" is a tool for fixing your project or figuring out necessary changes. Except in go, apparently.
- discovering new major versions in your go.mod (gomajor list)
- switching between major dependency versions. (gomajor get)
- updating the major version of your own module. (gomajor path)
https://github.com/icholy/gomajorIt's not perfect, but it takes a lot of toil out of working with SIV.
Two pain points stand out to me in the article though:
> Users of packages aren’t alerted about new major versions
> For the client to update, it’s not a simple path change in go.mod
The tooling should definitely be enhanced to make these things easier to do - probably a flag for -u to update to major versions, and a builtin script to globally update all the imports when a major version has changed.
Or maybe a way to specify a single 'default' major version in one place and then only needing to be explicit when using one that differs from that one. But this means you'd have files that could use different package versions if moved to a different project which could be unexpected.
It seems people rush to publish version 1.0 (or 2.0, etc.) of their libraries, when they'd be better off just sticking with version 0.x.
It's not like that a package isn't ready for production because it is < 1.0. It might as well be. If you're an early adopter, I'd say that is even welcome: you're aware that its API might chance, that its quite new, etc. It gives more confidence than a package with version 7.x (at least in informing you that it's prone to changes, and allowing you to make an informed choice), IMHO.
I think we should just push for more date based versioning, for instance, CalVer[0]
[0]: https://calver.org/
If you don't need automatic updates, then any kind of versioning is fine. Hell, you could get away with just using a single incrementing integer. v1, v2, v3, ..., v225883, etc.
The only thing loose dependencies and automatic version resolution and duplicating libraries achieve is a false sense of security when upgrading code. And the only thing Go's awful, awful idea to force v2+ to change import path is to make a mess in your version control history, and hide the important bits in a sea of needlessly changed files.
> this whole idea of automatically updating dependencies is awful.
Yeah, well, that's just, like, your opinion, man.
It's not SemVer(tm).
That's it. That's the whole pitch.
There's also a section specific to golang, which indeed advocates for sticking with version 0.x.x for a very long time: https://gist.github.com/warpfork/98d2f4060c68a565e8ad18ea481...
It doesn’t affect the package name (unless you choose to), only the import path. So it eliminates any ambiguity about the interface you intend to use and it only affects import statements. Personally I think that’s a good balance.
So this versioning thing is just weird and I agree with the author. It's a strange thing to have opinions on and an even stranger opinion to have on that thing.
But the thing that gets me is the whole putting "github.com/username/module" into code is just awful.
com.arbitrary.x.y.z ?
import “username/module”
And in the go.mod file the whole URL can stay require github.com/username/module v1.2.3
I only see advantages with this approach and I imagine it’s a rather “easy to add” feature that can be even backwards compatible.> Conflict resolution within the same module could be handled in the go.mod file:
> `require alias github.com/...`
Conflict resolution within the same module could be handled in the go.mod file:
`require alias github.com/...`
It's a joy.
Importing modules from a particular URL is a great way to handle them. No central server required! Everyone can name their module "utility utils" or whatever. And, nobody is forcing you to use Github to host your modules, you can put them on any web server you control and import them as example.com/your-thing. Decentralized. Easy to use.
If I had two complaints, they would be:
1) Library authors that think "replace" directives propagate up to consumers. They do not. They are ignored when you depend on the module. If you depend on one of these libraries and want the workaround that the author has smoothed over with a "replace" directive, you have to copy that directive to your own go.mod.
2) Library authors that distribute one go module for their super-complicated server and their super-simple client. This results in unnecessary dependency explosion. (Loki 1.x was an example of this pattern. Their server depends on things like Kubernetes, where their client only depends on net/http. But if you naively import their client, suddenly your project depends on Kubernetes.)
(2a would be no concept of test-only modules. Some modules are only needed for the tests; it would be nice to not propagate these up to consumers. It isn't an actual problem, though, just a "would be kind of nice" or "I could see why they did that" if it existed.)
Neither of these are Go issues, just maintainer issues that can happen with any language.
If you use a hosted service like github, you might still find it breaks when you do something like change the case on your username.
Everything on the Internet is vulnerable to this. NPM has gone down. You can send NPM a legal request to delete a module. Your domain registrar can take your domain name. Your local IP address registry can take away your IP address. At the end of the day, you can vendor all your modules and keep them with your source code if this worries you. It's not really in scope for a programming language to make a completely immutable and takedown-proof Internet just to let Internet users share some code with each other. But, they've done a pretty good job here.
The `go` tool. "Language runtime" refers to the ~2MB of thread scheduler, garbage collector, etc.
> No central server required!
Now you have five proxies to choose from, because coupling your project to a bunch of strangers' version control systems is bananas.
> Neither of these are Go issues, just maintainer issues
In practice, separating downstream quality-of-life from the language proper seems to breed chaos.
Example: https://github.com/golang/go/issues/24661
Until I have an extremely Go-shaped problem, I don't need that headache.
Every non-tiny place I have worked finds it necessary to copy known working versions of code they rely on, to an internal server and build from there. You don't have reproducible builds, for example, from a remote sever (maybe there are hashes?). But even then you don't have reliable access to external servers (they may crash, be bought, be hacked, change business models). So people self-host what they use.
Can you do this in Go, without changing source code?
========================================================
Not related to above, your 2) reminds me of my pet peeve: doc dependencies included in module dependencies. Oh, you use Sphinx, we'll install that. Oh, Sphinx needs Python, we'll install that. Try to use a simple, non-Python, library, get Python and all it's dependencies.
Dependency explosion is a real, really bad thing.
Yeah, IIUYC it even has first class support: `go mod vendor`
Maven naming convention is absolutely atrocious. And don't tell me that naming convention is optional. To be part of Java ecosystem Maven naming convention is a requirement.
It used to be pretty terrible. Today it's at least as good as npm.
> Did no one on the early design team ever deal with depenencies?
It was developed at Google and they have a giant monorepo, associated infrastructure, as well as company policies against multiple versions of dependencies. They also vendor external deps into this repo. So no, Google deals with dependencies a lot, but they do things very, very differently from almost everyone else.
https://news.ycombinator.com/item?id=24429045
The post itself
That’s what branches and tags are for, so what am I missing?
go mod init example.com/program
go mod tidy
go build
The go tool will see the "github.com/foo/bar" path in the import statements, download the code from the repository, compile and everything will work.Now, let's say the "github.com/foo/bar" module gets a backwards-incompatible change, but does not change the import path, and you attempt to do the above process again. This time, the go tool will download the incompatible version of "github.com/foo/bar" and the build will fail - or even worse, succeed but have some logic bugs that will go unnoticed until some massive shit happens.
I will agree that the ergonomics of this method aren't perfect by any means. But what it does accomplish, is that it forces you to declare your dependency explicitly (which seems to be one of Go's underlying principles).
A package may, or may not, decide to have a stable API and document it. If it does, and it commits to the version 1.0, then it basically gives a promise: "This package will not change its API in a way that will break correctness of currently-correct programs that use it".
Since a package == a directory, if you want to keep package compatibility, you must not change the contents of the import directory. Therefore, you need to create a new one, preferably called v2/, to put the new code in.
It's not a lazy solution, just opinionated.
Personally, I love the fact that just by seeing the import path I know exactly which codebase is used. I don't have to open my go.mod, see which version I'm using, clone the repository, dig through the history to check out a specific tag and see what code is actually used in my program... I just open the repo in my browser and browse through the directories. There isn't a single system on earth that doesn't support directories!
I also love the fact that I don't have to know s**t about git to use Go. If Go was to suddenly switch to git tags, not only would it break a massive amounts of existing code, but would basically force everyone to learn about git tags just to be able to see what code are they using, which would raise the amount of paperwork I have to fill in order to work on the thing I care about. Go is fundamentally against needless paperwork.
Browse via what? It would be entirely reasonable, not weird at all, if git.example/user/repo/ showed a list of branches and tags. And then it would actually be easier to reach git.example/user/repo/v3/ than to reach git.example/user/repo/master/v3/
The particular way github does directories isn't canonical. And if you wanted to clone the repo, you could clone a specific branch easily.
When it comes to minor versions, you have to dig anyway to see the matching source under the current system.
Via whatever tool you have available. Most projects also have a download link or a clone command written, in case the remote repo doesn't support in-browser file listing, so you can download it and browse locally, without needing to checkout tag or whatever.
> if you wanted to clone the repo, you could clone a specific branch easily.
Unless I don't know how to clone a specific branch - in which case I need to learn about git branches, figure out how they work, and how to clone a specific branch, and how to go back to master etc. etc. etc.
In contrast to just cloning the repo and browsing the v2/ directory.
> In contrast to just cloning the repo and browsing the v2/ directory.
"just" cloning the repo? You gave this list of arduous steps for why cloning a tag is harder than browsing, but "just" cloning the repo still makes you do all but one of those steps!
And if something is generating the clone command for you, it can add "-b".
You don't need to know how git branches work. You definitely don't need to know how to go back to master.
Absolutely not true. If you can "browse a repo", then by definition you can browse a directory inside a repo, so by definition you CAN do it. The option to choose a branch may or may not be there.
> "just" cloning the repo? You gave this list of arduous steps for why cloning a tag is harder than browsing, but "just" cloning the repo still makes you do all but one of those steps!
When you install a package from a remote repository, the go tool clones it to ~/go/pkg/mod/, so you can open it locally and browse through the code, without even touching git. So yeah, it is "just" cloning the repo. Also it works with any popular VCS, not just git.
Again, you're assuming that it shows master first.
If it shows the list of branches first, then it's easier to find a branch than it is to find a subdirectory.
Both are equally valid ways to show a git repo.
> When you install a package from a remote repository, the go tool clones it to ~/go/pkg/mod/, so you can open it locally and browse through the code, without even touching git. So yeah, it is "just" cloning the repo. Also it works with any popular VCS, not just git.
Nothing says it has to use that method. The other way could be just as streamlined, if that was the layout people wanted to use.
This is a bizarre statement. You only care to look at v4 or v7, not nor whether you're looking at v7.5.9 that is actually used in your program, or at v7.6.4 which you never upgraded to?
> I also love the fact that I don't have to know s*t about git to use Go.
Go is actually about the only language that forces you to know Git (or other sccs) to do dependency management. Just look at how horrible it is to develop and release multiple Go modules from the same repository, requiring specific tag names and who knows what else.
Go mod is a terrible kludge and virtually every decision they've taken is simple only for the Go implementation team.
Use tags or branches.
You also need to do this if the url for the package (for example if the repo changes to a different hosting service,or is moved to a different organization), or if you want to build with a fork at a different url.
This avoids all that v2, v3, v4 "nanny knows best" nonsense.
Unless your version numbers signify something to the user, you might as well just use incremental integers - v1, v2, v3, etc.
In the Golang case, encoding this is smart, especially since Golang's structural typing makes it easier to use. However, there is an ergonomics problem. I knew of all these things and I still had trouble upgrading a Golang FUSE driver recently. It's worth keeping in mind that I don't write code as a matter of course.
Bump Z up if the parameter arguments are the same but can take more argument options or a new API was added.
Bump Y up if an existing function argument types got fundamentally changed.
Bump X if it is a major rework, entailing new test suite, deleted obsoleted functions, engine redesign.
Of course, you should do doing some form of versioning check for same library across disparate subsystems (frontend, backend, middleware).
No more breakage.
This is a lame problem. v0.Major.Minor-Patch. Done. Yes, semver includes an optional dash for a fourth parameter, and Go supports it.
That's correct.
(The really fun weird corner of semver is build suffixes. According to semver 2.0.0, v1.2.3+4 and v1.2.3+5 have no specified relative ordering. According to semver 2.0.0-rc.1, build suffixes are ordered.)
Well, that doesn't seem to be true as Qvault is clearly a Wordpress powered website.
Quite common for the sales website to be wordpress and separate from the frontend of the product.
> this is redundant because the local go.mod file already has the semantic version of all dependencies tracked
The whole point of having full version path in each file is to allow gradual upgrade to a new major version/a completely different package. Listen to Russ Cox go over this many times.
> Users of packages aren’t alerted about new major versions
Fair point.
> For the client to update, it’s not a simple path change in go.mod
It's a simple command:
gofmt -w -r '"github.com/logrusorgru/aurora" -> "github.com/logrusorgru/aurora/v3"' ./
> Maintainers should be able to increment the major version via Git tags.
Go is not tied to git or any other single SCM system. They can however be pragmatic about this.
> I get why these rules exist, and I think they are great for large open projects
You either don't, or you think that smaller/trivial project's requirements somehow take priority over larger ones. There are plenty of toy/application specific/scripting programming languages built to be used to write a few hundred lines or glue things together. Go specifically and repeatedly states that it's aimed at large, distributed and long term projects with lots of changes over time and many contributors. You know that going in. So you're criticizing (poorly I might add) the very notions that this language is built upon! This is like criticizing an industrial kitchen equipment for not having pretty colors and curvy shapes to match your home decor.
That only works if v2 of the library is crazy enough to keep all of the v1 apis around as well. Otherwise, when I switch version the old import path stops working.
> gofmt -w -r '"github.com/logrusorgru/aurora" -> "github.com/logrusorgru/aurora/v3"' ./
Cool. Does that also cleanup git history so that I don't have to look at hundreds of files which needlessly got changed?
Gradual upgrade, as in you can update a single file to use the next major version. Not sure what you’re on about.
> hundreds of files which needlessly got changed
If you have hundreds of files and upgrading to a new major version all at once, you’re creating the exact risk that Go is trying to mitigate.
Niche people that depend on the old API will stick to an older version and be happy with that.