5 ms·
In my opinion, not allowing circular dependencies is a great design choice for building large programs. It forces you to separate your concerns properly. If yo
by nickcw 1y ago
In my opinion, not allowing circular dependencies is a great design choice for building large programs. It forces you to separate your concerns properly.
If you get a circular dependency something is wrong with your design and the article does a good job on how to fix them.
I sometimes use function pointers which other packages override to fix circular dependencies which I don't think was mentioned in the article.
My only wish is that the go compiler gave more helpful output when you make a circular dependency. Currently it gives a list of all the packages involved in the loop which can be quite long, though generally it is the last thing you changed which caused the problem.
- ternaryoperator 1y agoIn the abstract, I think I agree with you. But in reality, what I see is that go projects use far fewer packages than, say, programs in Java. Many go projects use one or two omnibus packages--principally, I expect, to avoid having to worry about circularity issues. By forcing this design pattern on developers (something no other language does), I think the result has been overall worse code rather than better. Perhaps a warning, rather than stop-the-compiler error would have been a better choice. Not sure. Either way though, I wholly agree that the compiler gives too little information, which is curious because it knows the needed data and should easily be able to present it in a useful way.
- jen20 1y agoI see the opposite in most Java programs: poor organization with packages based on a type of thing (eg models, controllers) rather than related behaviors. Go doesn’t have warnings, which is great - if something is worth warning about, it is also worth erroring about. It never ceases to amaze me when a brand new JavaScript project spits out dozens of errors after pulling in a common library, and everyone thinks that is ok.
- ncruces 1y agoOf course Go has warnings. It has go vet, which is not a linter, and according to the authors doesn't need comments to ignore checks, because the checks are always correct about you having written shitty code. Except where it might warn you about something completely outside your control.
- jen20 1y agoWell then why not just integrate it into the compiler and make them errors?
- wavemode 1y ago> If you get a circular dependency something is wrong with your design Packages not being able import from each other circularly is purely a compiler limitation. It says nothing about the realities of software development. This notion stems from the idea that software design is inherently hierarchical, and that there is always a clear "higher level" and "lower level" between every possible software module. What I've found in practice is that this is a fictional concept. Circularity between modules is very common and natural (especially as business requirements change over time). The workarounds people invent to avoid circularity literally always result in a codebase that is harder to understand and maintain, rather than easier. > It forces you to separate your concerns properly. Nah. It's not separation of concerns, it's separation of implementation. Two functions that in every other way shape and form deal directly with the same concepts, end up needing to be in separate modules purely because they differ in the functionality they import. And if later their imports change, they may need to be moved again. Which means implementation details are leaking into your design, which makes code less discoverable (since you now need to know implementation details in order to reasonably predict where a given function might be defined).
- politician 1y agoI prefer extremely fast compile times.
- LtWorf 1y agoIt's not fast if it fails every time you commented a line because a variable becomes unused though.
- homebrewer 1y agoOther languages (Pascal, Ocaml, maybe Zig) have proven that it's possible to implement a very fast compiler that emits efficient machine code while not dumbing down the language to complete brain death.
- flavio81 1y agoAdd Common Lisp to the list. And even Scala has fast compile times today.
- layer8 1y ago[The following is intended as language-agnostic.] It’s useful to distinguish between interface and implementation dependencies. I agree that there shouldn’t be circular interface dependencies between modules. The absence of circular interface dependencies allows separate compilation of modules. It also means that at least in principle, the implementations can be made non-circular (can be refactored to non-circular without breaking any of the existing interfaces). But it’s often okay for the implementation of A to depend on the interface of B, and at the same time the implementation of B to depend on the interface of A, as long as there is no mutual dependency between the interfaces of A and B.
- euroderf 1y ago> In my opinion, not allowing circular dependencies is a great design choice for building large programs. I have a hobby project with maybe 20 packages involved. Circular dependencies were getting harder and harder to solve. What worked in this situation was to separate the whole hairball into two layers. The packages in the app layer could import packages from the utilities layer, but not vice-versa. This introduced enough structure to simplify the removal of circularities and prevent new outbreaks.
- ncruces 1y agoBut, don't you know you can't have a utils package either? A package must implement one functionality, and it must be clear from the short import name what that functionality is. A package is also the only way to enforce visibility (and often, with that, other useful properties like immutability). So packages can't be too big, or too generic, but they can't also be too small, or too specific. I also have 2 (or 3?) layers, and a utils package, and the packages are huge, and yet I need various cheats to allow cyclic dependencies, and have some repetition with other tricks to avoid some of it. It's the real world, and guess what, the very well designed standard library does… all of the above too.
- Osiris 1y agoWhat do you mean by "can't"? "Can not" means "not possible", but it's clearly possible.
- ncruces 1y agoI obviously meant "can't" not RFC 2119 "MUST NOT". I could list more sources, but I'll just leave you with these: https://go.dev/blog/package-names https://go.dev/blog/package-names https://google.github.io/styleguide/go/best-practices#util-packages https://google.github.io/styleguide/go/best-practices#util-p... https://dave.cheney.net/2019/01/08/avoid-package-names-like-base-util-or-common https://dave.cheney.net/2019/01/08/avoid-package-names-like-... The Google style guide is worth a read through, as the subsequent point recognizes the difficulties around package size. It can make sense to have a package with a single function, and it can almost make sense to stuff your entire program/library in a single package. The two rules work that exacerbate this tension are precisely visibility and no cyclic dependencies. The perfect is the enemy of the good. Forbidding cyclic dependencies is one such case, IMO.