7 ms·
> it's pretty difficult to over engineer I don't know about that. Every programmer's first Go program seems to like to go to channel city. Perhaps more accurat
by randomdata 2y ago
> it's pretty difficult to over engineer
I don't know about that. Every programmer's first Go program seems to like to go to channel city. Perhaps more accurately: Over-engineering your Go program is going to quickly lead to pain. It doesn't have the escape hatches that help you paper over bad design decisions like some other languages do.
- nasretdinov 2y agoYeah too much concurrency and too many channels definitely hit home hard...
- arp242 2y agoAlso: interfaceiritus. Someone saw "accept interfaces, return structs" somewhere and now EVERYTHING accepts an interface, whether or makes sense or not. Many (sometimes even all) of these interfaces have just one implementation.
- hellcow 2y agoDoing this allows you to mock out that implementation in unit tests.
- arp242 2y ago> Many (sometimes even all) of these interfaces have just one implementation.
- kbolino 2y agoThe point of using a mockable interface, even if there's only one real implementation, is to test the behavior of the caller in isolation without also testing the behavior of the callee. This can be overdone of course, not everything needs this level of separation, but if it makes testing one or both sides easier, then it's usually worth it. It's especially useful for testing nontrivial interactions with other people's code, such as libraries for services that connect to real infrastructure.
- randomdata 2y agoDid you miss "just one implementation"? A mock is literally defined by being another implementation. If the 'mock' is your sole implementation, we don't call it a mock, that's just a plain old regular implementation.
- deleted 2y ago[deleted]
- kbolino 2y agoI think my comment was clear on the distinction between real and mock implementations. If the code was testable with no need for mocks then certainly remove the interface and devirtualize the method calls.
- randomdata 2y agoYour comment was clear about mocks, but not why mocks are relevant to the topic at hand. The original comment was equally clear that it was in reference to where there is only one implementation. In fact, just to make sure you didn't overlook that bit amid the other words, the author extracted that segment out into a secondary comment about that and that alone. Mocks, by definition, are always a supplemental implementation – in other words, where there is two or more implementations. What you failed to make clear is why you would bring up mocks at all. Where is the relevance in a discussion about single implementations the other commenter has observed? I wondered if you had missed (twice!) the "one implementation" part, but it seems you deny that, making this ordeal even stranger.
- kbolino 2y agoIt is easy to generate mock implementation code (GoMock has mockgen, testify has mockery, etc.) The lack of a hand-rolled mock implementation doesn't mean that much. For example, many people do not like to put generated code under source control. So, just because you don't see a mock implementation right away doesn't mean one isn't meant to be there. Also, the original author of the function that consumed the apparently unnecessary interface type may have intended to test it, but not had the time to write the tests or generate the mocks. If we are going to be this pedantic, I did say "mockable" interface, implying the usefulness and possibility, but not necessarily existence, of a mock implementation. Since we are examining code we can't see, we can only speak about it in the abstract. That means the discussion may be broader than just what one person contributes to it. If this offends you or the OP, that was not the intent, but in the spirit of constructive discussion, if you find my response so unhelpful, it is better to disregard it and move along than to repeat the same point over and over again.
- throwaway2037 2y agoI agree with your point. OP wrote: > Many (sometimes even all) of these interfaces have just one implementation. They are missing that mocks are the second implementation. (It took me years to see this point.) I would say that in most of my code at work, 95+% of my interfaces only have a single implementation for the production code, but any/all of them can have a second implementation when mocking for unit tests.
- tengbretson 2y agoA lot of times you want to be able to cmd+click on something and actually see what the hell the code actually does and not get dead-ended at an interface declaration.
- zinodaur 2y agoyeah gotta use an IDE for that
- randomdata 2y agoSounds like a UI bug more than anything. The compiler certainly knows how to determine if there is only one implementation of an interface and remove the interface indirection when so. There is nothing really stopping the cmd+click tooling from doing the same.
- anonymoushn 2y agoDoes the compiler do that? That sounds extremely unlikely, especially because an interface with only one implementation can store the nil type tag or a tagged pointer to an instance of that implementation.
- randomdata 2y agoThe nil interface is another implementation. I mean, unless it is being used as the sole implementation, but I think we can assume that isn't the implementation being talked about given that it isn't a practical implementation. We're talking about where there is one implementation.
- anonymoushn 2y agoRight. Can you cite anything that says that the go compiler does this sort of whole-program analysis to try to prove that a certain argument to a function is always non-nil, so that it can change the signature of that function and the types of variables declared in other functions?
- neonsunset 2y agoCan't Go compiler statically prove that such single implementation interfaces are indeed that and devirtualize the callsites referring to them? Either way, the problem seems to happen in most languages of today, if they (or their community) ever happen to accidentally encourage passing an opaque type abstraction over a concrete one.
- deleted 2y ago[deleted]
- nasretdinov 2y agoI think it actually does that, but in local contexts, where this analysis is somewhat easy. I also believe you don't actually have to prove it statically: PGO can collect enough data to e.g. add a check that a certain type is usually X, and follow a slow path otherwise
- neonsunset 2y agoI understand that it does so when the exact type is observed - a direct call on a concrete type. But I was wondering if it performs whole-program-view optimization for interface calls. E.g. given a simple AOT-compiled C# program: using System.Runtime.CompilerServices; var bar = new Bar(); var number = CallFoo(bar); Console.WriteLine(number); // Do not inline to prevent observing exact type [MethodImpl(MethodImplOptions.NoInlining)] static int CallFoo(Foo foo) { return foo.Number(); } interface Foo { int Number(); } class Bar: Foo { public int Number() => 42; } On x86_64, 'CallFoo' compiles to: CMP byte ptr [RDI],DIL ;; null-check foo[0] MOV EAX,0x2a ;; set 42 to return value register RET There is no interface call. In the above case, the linker reasons that throughout whole program only `Bar` implements `Foo` therefore all calls on `Foo` can be replaced with direct calls on `Bar`, which are then subject to other optimizations like inlining. In fact, if we add and reference a second implementation of `Foo` - `Baz` which returns 8, `CallFoo` becomes ;; calculate the addr. of Bar's methodtable pointer LEA RAX,[DevirtExample_Bar::vtable] MOV ECX,0x8 ;; set ECX to 8 MOV EDX,0x2a ;; set EDX to 42 ;; compare methodtable pointer of foo instance with Bar's CMP qword ptr [RDI],RAX ;; set return register EAX to value of EDX, containing 42 MOV EAX,EDX ;; if comparison is false, set EAX to value of ECX containing 8 instead CMOVNZ EAX,ECX RET Which is effectively 'return foo is Bar ? 42 : 8;'. Despite my criticism of Go's capabilities, I am interested in how its implementation is evolving. I know it has the feature to manually gather a static PGO profile and then apply it to compilation which will insert guarded devirtualization fast-paths on interface calls, like what OpenJDK's HotSpot and .NET's JIT do automatically. But I was wondering whether it was doing any whole-program view or inter-procedural optimizations that can be very effective with "frozen world single static module" which both Go and .NET AOT compilations are. EDIT: To answer my own question, I verified the same for Go. Given simple Go program: package main import ( "fmt" ) func main() { bar := &Bar{} num1 := callFoo(bar) fmt.Println(num1) } //go:noinline func callFoo(foo Foo) int { return foo.Number() } type Foo interface { Number() int } type Bar struct{} func (b *Bar) Number() int { return 42 } 'callFoo' compiles to CMP RSP,qword ptr [R14 + 0x10] JBE LAB_0108ca68 PUSH RBP MOV RBP,RSP SUB RSP,0x8 MOV qword ptr [RSP + foo_spill.tab],RAX MOV qword ptr [RSP + foo_spill.data],RBX MOV RCX,qword ptr [RAX + 0x18] ;; load vtable slot? MOV RAX,RBX NOP CALL RCX ;; call the address loaded from the vtable? ADD RSP,0x8 POP RBP RET LAB_0108ca68 XREF[1]: MOV qword ptr [RSP + foo_spill.tab],RAX MOV qword ptr [RSP + foo_spill.data],RBX CALL runtime.morestack_noctxt MOV RAX,qword ptr [RSP + foo_spill.tab] MOV RBX,qword ptr [RSP + foo_spill.data] JMP main.callFoo It appears that no devirtualization takes place of this kind. Writing about this, it makes for an interesting thought experiment what it would take to introduce a CIL back-end for Go (including proper export of types, and what about structurally matched interfaces?) and AOT compile it with .NET. [0]: VMs like OpenJDK and .NET make hardware exception-based null-checks. That is, a SIGSEGV handler is registered and then pointers that need to throw NRE or NPE either do so via induced loads from memory like above or just by virtue of dereferencing a field out of an object reference. If a pointer is null, this causes SIGSEGV, where then a handler looks if the address of the invalid pointer is within first, say, 64KiB of address space. If it is, the VM logic kicks in that recovers the execution state and performs managed exception handling such as running `finally` blocks and resuming the execution from the corresponding `catch` handler.
- ukoki 2y agoAgreed. It's not as 'traditional Go' but I find there is way less interface boilerplate if you just pass functions around. ie instead of ``` type ThingDoer interface { DoThing() } func someFunction(thingDoer ThingDoer) { ThingDoer.doThing() } ``` just have ``` func someFunction(doThing func()) { doThing() } ``` Then when testing you can just pass a test implementation of the 'doThing' function that just verifies it was called with the expected arguments.