I always wondered how that would work in a medium or large codebase.
Is it easy to refactor? Refactoring can change the interface of classes, which is easier if they are explicit.
And my main fear: how do I know I use the right object? I would tempted to add methods all the time to satisfy the target interface instead of using another object that already satisfies that interface.
Not knowing explicitly whether I can use an object or not is confusing, or am I missing something obvious?
Edit: Last but not least, how do I know my classes implement an interface that could require 5 or 10 methods? I had to do that in Go, and counting and searching the methods was really a waste of time, so much that I had to add comments to explain the interfaces that were implemented in every class.
I haven't done much Go, but TypeScript has similar characteristics with its interfaces and I haven't found refactoring to be much of an issue. If the interface changes then the type checker will alert you to all the places where you now have the wrong method type. The main difference between this and a Java-style explicit interface is that you have to make one jump from the error at the point of use to the definition of the class or record, where in Java the error points you right to the class.
> And my main fear: how do I know I use the right object? I would tempted to add methods all the time to satisfy the target interface instead of using another object that already satisfies that interface.
Why don't you do this with explicit interfaces? It's one extra line of code beyond the method implementation, and it's a backwards-compatible change. I suspect that the main reason you don't is because you know that class Foo shouldn't be a Bar, and the same logic can easily guide you with implicit interfaces.
> Not knowing explicitly whether I can use an object or not is confusing, or am I missing something obvious?
This one I'll agree with. Implicit interfaces work well in quick code that you're writing yourself that fits in your head. When you're trying to understand a library someone else wrote, though, being able to have the IDE list all the implementations of an interface is very valuable.
Also, explicit interface tagging communicates authorial intent in a way that isn't possible with implicit interfaces.
Yes. In particular, it means you can switch from depending on a concrete type (say, a whole S3Client) vs depending on just a much smaller interface (eg if you only want GetObject) without having to touch anyone else’s code. The compiler will still check that the types being assigned to the interface meet those constraints.
Sounds a lot like just using functions. I do that a lot if there is only a single function from a type I need. I’m yet to decide if that’s a mistake or not.
I think the dark side of it is unintentional ADL but it's mostly good. C++ gets a lot of mileage out of concepts in templates. The newer language feature just makes the existing practice first class as something a bit beyond syntactic sugar. But the design of the STL relies heavily on the idea that if something has the requisite properties it is that thing. Iterators are probably the most prominent example. You won't find any explicit iterator types. It's just a set of properties that a type has that makes it an iterator or not. Personally I've never seen confusion be an issue. Granted the iterator and algorithm design has been mostly superceded by newer apis that operate as pipes of functions over streams. Frankly the experience of these are poor in C++ for various reasons.
One big example is that keys in associative collections are const qualified even when moving from the collection. The constness doesn't match the expectation users have when consuming a collection and is unfixable within the constraints of the STL. Anyway it results in awful type checking errors. The whole library is full of these foot guns and most of them result in bizarre behavior or horrible error messages.
Iterators have their own warts but IMO work much better within the C++ type system. Here's a fun one. Reverse iterators have their own set of invalidation properties which are typically weaker and different than forward iterators. Due to various reasons they actually refer to the element that precedes (in reverse sequence) the one you'll get when dereferencing it. So end is rfront and front is rend.
In either case the experience is quite bad compared to the stream apis you get from rust. But I don't think this is a mark against concepts, just a dated design and the limits of the type system and semantics of the language.
We've switched a lot of our C# to Go because it works better for a lot of our concurrency computation, and I've found that it's easier to refactor because you have a single implementation of an "interface". I say that in quotes because Go's interface{} isn't really like a C# interface and what you'd use for "classes" in Go is typically structs.
I think it's easier to think of Go as a mix of Python, Typescript, C++ and others, making sort of the same re-implementation that Java/C# originally did with a more modern approach. Please not that this is neither completely correct and opinionated, but I think it's a good way to explain it. Similarly I think Typescript is a better way to think of objects in Go than what you may be used to coming from C#. Stucts work much like Type/Interface in Typescript and you're not going to have issues with it because anything you change will be immediately obvious in your code. It also means your functions live in isolation from the objects, and this is perhaps one of the "weaker" parts of Go coming from C# because it won't be blatantly obvious when you're working with an object until you get the working behind = assignment and := declaration + assignment. On top of that you have the Go interface{} which isn't really comparable to a C# interface and it's much easier to think of it as the Typescript "Any/Unknown" type. This isn't exactly true because it's an unknown type where all you basically know is that it holds no methods, meaning that unlike the Any type in Typescript and somewhat similar to the Unknown type {} is actually useable in Go.
I don't think there is a good reason to chose C# for new projects if Go is an option for you. I don't think there is any reason to use Go if you plan on using C#, maybe because that is what your team does well. We did it because we needed coroutines easily and because most of our programmer aren't really C# programmers but Typescript programmers. We found it to be a delight to work with, however, but realistically I don't think there will a reason to adopt Go very often if you're big on C#/Java. At least unless the landscape of the talent pool changes into Go orientated, as it'll typically be much easier to hire and onboard people from C#/Java.
This sounds rather crazy and unrealistic, as C# with its task system and threadpool implementation is strictly better at massively concurrent and parallel applications.
Go in general is a poor, bad language with unsound type system, significantly higher amount of footguns and much worse throughput scaling than .NET.
.NET truly is in “casting pearls before swine” predicament if that’s how some of its users see it.
Note that if you look at GitHub statistics - Go has already won popularity-wise because it’s not the technical merit that matters nowadays but “vibes”, which is to say no amount of bullying is sufficient until Go community stops damaging technical landscape.
You're not really making a lot of argumentation to back up your claims. C#'s TPL is ok, but it comes with a massive overhead compared to Goroutines where you can have millions of concurrent threads running at the same time, which is just immensely useful when you're dealing with lots and lots of data. Like gathering solar plant data from millions of inverters. On top of that Go comes with build in channels to ensure safe synchronization between your Goroutines. It's not that you cannot write concurrent code in C#, but to do so will involve a higher level of complexity as well as a much higher cost in memory usage and the risk of encountering deadlocks.
For us the major advantage is that it's much more efficient to spread out the computation on multiple CPU's rather than relying on OS or thread pool threads while also lowering the risk of someone writing a bottleneck when they are coding on a thursday afternoon after a day of horrible meetings.
> GitHub statistics
I think Github statistics are meaningless. My github repository is 100% rust. I very rarely use Rust professionally. In fact, I've done precisely one proof of concept before we decided it was too much trouble to adopt it instead of our C++. This may change in the future if the Rust "community" matures. In any case I mostly look at job "statistics" for my area of the world and while Go has been adopted at some of the places I might want to work, it's still a drop in the ocean of java/c#/php/python.
> C#'s TPL is ok, but it comes with a massive overhead compared to Goroutines
The only possible way to make this statement with confidence is never measuring the overhead of tasks and goroutines. Tasks are lighter than goroutines, especially memory-wise. Don't trust me? Write any sample code you consider representative. Try spawning one million tasks and then one million goroutines, look at memory consumption and execution time. Surely the results will be as you say, right?
Also, .NET has channels out of box and they are being used where they make sense (which turns out to be not for every second thing even if it's unidiomatic, something about hammer and things looking like a nail).
In general, I think you either never worked with .NET at all or never understood it beyond surface level impression, and rely solely on urban myths about the topic of this discussion. Because there is no other way to explain the abstract "bottleneck" you imagine nor thinking as if Golang's runtime isn't a work-stealing scheduler just like .NET's threadpool, which it is.
> The only possible way to make this statement with confidence is never measuring the overhead of tasks and goroutines. Tasks are lighter than goroutines, especially memory-wise.
> In general, I think you either never worked with .NET at all
What a wild assumption to make, so I'll maybe counter it with the same claim for Go because what you write here isn't really true. It might be if you use standard goroutines which run with 8 KB stacks (even worse on Windows where you're paying an additional 4k because... well because Windows), but you both can and should modify this.
> Try spawning one million tasks and then one million goroutine.
I can run around 50 million goroutines on my macbook air. I can barely run 100k tasks. Task which again are asynchronous, which comes with a range of issues that you conveniently skipped talking about. I think that it is fine that you love C# and apparently hate Go, but maybe your personal opinion is getting in the way a little?
This is impossible. Baseline allocation size for an asynchronously yielding task starts at ~100B (because synchronously completing task may not allocate at all, some tasks are extended with additional pooling mechanic to not allocate in all scenarios through pooling or otherwise, like async socket operations).
What sample do you use for comparison? Maybe your gorotines do nothing but sleep and tasks do active work? I imagine Go would struggle even with just single allocation per goroutine if they do anything more if it is literally >= 50M goroutines. For example, BEAM already starts to struggle with 1M, at least spawning-wise, and Go runs out of memory easily at 10-30M if the only modification is GOMAXPROCS (which is what realistically everyone does).
Also hot splitting is very fun to fix by randomly rearranging the code.
Is it easy to refactor? Refactoring can change the interface of classes, which is easier if they are explicit.
And my main fear: how do I know I use the right object? I would tempted to add methods all the time to satisfy the target interface instead of using another object that already satisfies that interface.
Not knowing explicitly whether I can use an object or not is confusing, or am I missing something obvious?
Edit: Last but not least, how do I know my classes implement an interface that could require 5 or 10 methods? I had to do that in Go, and counting and searching the methods was really a waste of time, so much that I had to add comments to explain the interfaces that were implemented in every class.