 I know that mine can fail and I'm a software engineer. Hi, I know that mine can fail. And today we're going to talk about how our mindset needs to change when coding in Go versus in OOP languages. Let me start by introducing myself. I'm a software engineer at Workout. We're working on building a debugger for production. I come from a background of low-level programming in C and Linux IoT environments. And I really love learning new technologies, new programming languages, and especially how they work behind the scenes and their internals. Now, Workout supports six different languages. And for each of those languages, we have a different product in that language. So I find myself translating a lot of code. One of my first tasks here was to translate a section of code from Java to Go. This talk is based off of that experience, the mistakes I made, and the tips and tricks I learned along the way. Now let's start by asking ourselves the question, what is OOP? Well, I know some people think it is a variation of these four principles, but these are the commonly regarded principles of OOP. So the first is abstraction, which means that we don't need to know how something works in order to use it. Basically, it's representing complexity through simplicity. Encapsulation is differentiating between things that should be publicly accessible to all or private or accessible to only certain objects in your code. Plomorphism is using different types through the same interface. And inheritance, since we are going to talk a little bit more about inheritance, I wanted to dive a little deeper on that principle. Inheritance is deriving a class from another class. This creates a hierarchy of classes where the child classes share attributes and methods with their parent classes. This helps organize code, reduce duplicate code, add new classes easily, and also extend existing libraries. So the big question is go OOP. Now, I know some people say definite yes. Some people say definite no. And these are the three things that I like to consider when facing this question. First of all, go has types, but no type hierarchy. Now, these types can also have methods on them. But even the simplest types can have methods. So types like int or string, which come built in the language, you can extend them with new methods. And lastly, interfaces are implemented implicitly. So interfaces do exist and go. They allow for polymorphism, an abstraction of some sort. But they are implemented implicitly. So the class or the struct that is implementing this interface doesn't even need to know of its existence. This makes interfaces much more lightweight than interfaces in other languages, OOP languages, such as Java or C-sharp. So they're also much more common in code. You'll find many more interfaces in go rather than in other OOP languages. So considering these three, we do see that go satisfies most of the OOP principles I mentioned. But a big principle that go doesn't satisfy is inheritance. And I don't just want to tell you this. I want to show you why inheritance doesn't work in go. We're going to take the example of a logging library. Just as I did, we're going to translate that logging library from Java to go and see where common mistakes and pitfalls can happen. So our logging library has two main requirements. First of all, we need to support multiple log levels. And we're going to have a function for each of those log levels. Secondly, we're going to want user defined logging targets. This means that by default, we will log to STD out. But if a user decides they want to log to a file or to a socket or basically anywhere, we'll allow them to do that and extend it easily. Now let's start by our Java implementation. Our design is going to be something like this. We're going to have our five log levels as functions. And eventually, each of those functions will call the write method. And the write method will actually write the log. So our Java implementation will be creating a logger class, having a write method, which just prints to STD out, and then creating a debug method, which calls that write method. Obviously, we need to add those four other log level functions. But just for the sake of fitting this all on one page, I decided to keep those out. But they look exactly like debug with different log levels. Awesome. So in order to use this logger, we can just create a new logger instance, call the debug method, and we're printing to STD out, exactly what we wanted. Now, if the user wanted to log to, say, a file, he could just extend the existing logger. So in Java, we have the extends keyword, which just inherits from the extended class. So in this case, logger in, sorry, file logger extends logger. So file logger is inheriting all of logger's attributes and methods. So we are going to override the write method. And so instead of writing to STD out, we're going to write to a file. Now, when we put that all together, we can create a new file logger instance instead of a new logger instance. And when we call the debug method on that, we're actually logging to a file. Great. But now we can go. So we're really going to translate this. But before we can actually start translating, we need to know a concept called embedded structs. Now, this is a very simple example. We have our ABC struct. This ABC struct has one field, i of type int, and one method foo. Once again, very simple. Now, in order to embed the ABC struct in another struct, we just need to write the name of the struct in the struct we're embedding it in. So in this case, we're embedding ABC in the my type struct. Now, what this causes is that ABC's fields and methods are promoted to my type. So my type now has access and can use these fields and methods exactly like ABC does. So we have access to the i field. We can set it, we can get it, and we have access to the foo method, which is exactly the same method that we have on ABC. Now, let's use this in our solution to our logging library. Once again, we're going to have those five logging level functions, and eventually they're all going to call the write method, which we'll actually write the log. So just like we created a class and Java, we're going to create a struct and go, a log struct. We're going to create a write method on that log struct and print test it out. And we're going to create a debug method, which calls that write method. Great. Now, putting that all together, we can create a logger instance, call the debug method and print test it out. And if we wanted to extend the logger once again and create a file logger, well, we could embed the logger type in the file logger struct. So just as we did here, I've embedded the logger struct in the file logger struct. In addition to embedding the logger struct, I've also added a file name field just so that we can keep the file name and maybe pass it once we initialize the file logger. So once again, as you probably know, in Go we don't have decorators or annotations like in Java, so we don't have that override annotation. So we'll just create the write method without that annotation and write to a file instead of to SDD out. Great. So let's put that all together. We'll create a file logger instance, set that file name to be our log file and then call the debug method. Now what we expect to happen is it for it to log to a file, right? But then what actually happens is that we're logging to SDD out again. So we have a bug here. Now in order to understand why our bug is happening, we need to understand how Go works. So first of all, creating a method in Go is not very different than just creating a function and passing the receiver as the first parameter. So in this case, we have the debug method and logger is the receiver, the receiver type. But it's exactly the same as creating a debug function and just passing the logger type as the first parameter. Now another thing we need to know about Go is that embedding a struct is not really like inheritance. It's more like creating a nice wrapper that Go does for us. So embedding a struct is kind of like just adding a field to the struct we're embedding in. So in this case, creating embedding the logger struct in file logger is not very different than creating a logger field on file logger. And then what about those promoted attributes and functions? Well, the debug method that now exists on the file logger because we embedded logger is not more than just a wrapper. So in this case, we can just wrap the debug function, the debug method on file logger and inside it just call the debug method on logger. So what's really happening when we embed a struct and another struct is Go is creating all these wrappers behind the scenes and doing all this without us even knowing which can be really neat at times that can also be kind of buggy in other times like this one. So let's put that all together and kind of figure out what really happened here. First of all, we created a file logger instance and we call the debug method on file logger. Then we reached this function, right? The debug method. And since it's a wrapper that Go created for us, what's actually happening is we're calling the debug method on the logger, not the file logger. So we've reached the debug method on the logger, right? This is the same function. So at this point, we have no semblance to the file logger. It's long forgotten. Logger doesn't know it was part of a file logger once. It doesn't know where it was called from and we've just lost the file logger context. So when we call the write method, we're just calling the write method on the logger. And so we reach the write method on the logger and we're printing to STDF and not to a file. So that was the practical problem with our code. But our real problem was that we were trying to translate code from Java to Go and we were trying to use an inheritance in general, which just isn't a Go concept. And the solution to this, how do we actually keep Go in mind when we're coding? Well, first of all, we need to get to know those features. And as we saw here, knowing the syntax just isn't enough at all times. For the basics, it might be good and there might not be some things behind behind those syntax. But when we get to more advanced concepts, just like embedded structs, we need to know how they work and what's actually happening there. In addition, we need to stop translating code and concepts. So I learned Java after I already knew C-sharp and translating concepts from C-sharp to Java really helped me get into the language faster and not have to learn everything from scratch. But when I do this with Go, I often find myself having these really hard to detect bugs because they're just not, these languages aren't meant to go hand in hand. They're not meant to be translated from one to another. So we need to rethink how we do things when we do them in Go. Now, I will say that for basic concepts like translating just a struct to a class, it will work. So keep those basics, but when we get to more advanced things, we do want to think them through and make sure we're doing the correct thing. And last but not least, we need to Google everything. So this is true for all programming languages, but I found this especially true for Go. No matter how long I code in Go, I still find that Google searching a very, very simple term might end up with a very different solution, which is much better, much simpler, works way better than my original solution. So really Google everything. Even if you think you know, you might not know. And now back to our logger. Let's talk about the practical logger solution. Well, let me start by saying that there's more than one solution to our logging library problem. Additionally, we're going to use interfaces in all of the solutions, interfaces. As I said, they're common, they're lightweight. Why not use them? Now, I will say that we want to not abuse interfaces. So use them wisely, make sure you're not creating an interface for just one concrete type. But in this case, we have the STD out target and the file target. And this is a great example of when we can use an interface, right? Let's go back to polymorphism. We're talking about representing, using different types through the same interface. And we want to return to our goal because our original design just wasn't good for Go. So once again, we're talking about functions for each log level and user defined logging targets. So like I said, a really good place for an interface is our logging target. So we're going to create a logging target interface with that right method in it. One method, very simple, very straightforward. A lot of Go interfaces are just one method and that's how they should be very lightweight. Now we're going to create a global variable called target and have that V of type logging target. In addition, we're going to create a debug function, right? And Go, we don't need all functions to be methods part of a struct. We can just have functions like that. So we're just going to have a debug function that calls the right method on our global target variable. Then we'll create our standard log target. We'll create that a simple struct, no fields with one method, the right method which prints to STD out. Then we'll create a file logging target. Once again, very simple struct. We might have a few fields in it. I didn't write them here, but we might want to use a file name field. Now I did want to create a constructor for our file logging target and Go we don't have constructors but I did create a new file logging target which I expect the user to use when they create a file logging target instance. That will just save our file name and let us have more control over how that file logging target is created. And then create the right method which just writes to a file. So now both file logging target and standard logging target are implementing the logging target interface. Great. Now the only piece missing is to be able to set the logging target. So I've created a set logging target function which accepts a new target as a parameter and that will just switch the new old target to the new. If I wanted to do anything else like validate the new target or something like that I could do it in this function. So having the user call this function instead of just set the target directly allows me more control over that, which is nice. So let's use this. We'll create a standard logging target instance set that to be our logging target call the debug function and we're logging to STD out. Great, the same way we can create a new file logging target instance call the set logging target function with that file target so set that to be our logging target call the debug function and we're logging to a file. So this is a great solution. It works. But as I said, we should Google everything. In this case, one Google search away is this interface. This is the IO dot writer interface in the IO package. The IO package is a standard package in Go. It comes with a standard library. Literally every Go binary out there has this interface already in it. So this interface is very, very similar to our logging target interface, right? They both have the right method and they accept different parameters and they return different values but they're basically the same. Right point and then it's the same. So let's just do one small change and have our target go from the logging target type to the IO dot writer type. And now we can use OS dot STD out. OS dot STD out write the STD out variable in or const in, no it's a variable in the OS package. It already implements the IO dot writer interface. So we don't have to create our own standard logging targets to implement the right method. We already have something in the standard library that implements that for us. And our implementation has become so much smaller, so much simpler. We don't have to write all this extra code which is just wrappers for functions and things that already exist. So once again, we can set OS dot STD out to be our logging target, call the debug method and we're printing to STD out. Now, like OS dot STD out for files we have the OS dot create function. The OS dot create function creates a file and returns some sort of file struct to us. Now, since the file struct already implements the IO dot writer interface we can use that to be our logging target, call the debug function and we're logging to a file. So that simple change really made a big difference. Now, because of that change we support logging to buffers, to sockets, to files and even to hashes if someone wanted to. Now, in fact, last time I checked the IO dot writer interface was implemented 351 types in the standard go library alone. So right, they're not all useful for our case, for our logger but still that small change, that small change made it so much simpler to use our code and users don't need to wrap existing behaviors in new functions and implement a new interface just to use our code. So once again, Google search everything. Okay, but what if we wanted to log to multiple targets or choose which target to use based on a context, right? What if we wanted to log both to a file and twist to be out or say we wanted one function to log to a file and one function to log to SDV out. What I'm getting at is that our logger needs to be an object. This will allow for more versatile use of our object. So we need to go back to embedded structs but this time we're going to add a little twist. Instead of embedding a struct in another struct, we're going to embed an interface in a struct. Now this is very similar and let's see how that works. Well, we're going to create a very simple interface call it my interface with one method, the flu method. Now, because we want a concrete type that implements this interface, we're going to create my implementation, very simple empty struct that implements my interface. Now, just as we embedded a struct in another struct, embedding an interface in a struct is just writing that interface's type, the name of the type inside the struct. So just as we see here, in this case, we're embedding my interface in my struct. Now we'll create an instance of my implementation. We need to do this. We need to have a concrete instance of a concrete type in order to embed an interface in a struct. And then when we create an instance of my struct, we're going to pass that implementation as the interface. So putting that all together, what that means is that calling the foo method on M and calling the foo method on I leads to the exact same behavior, okay? So this is very, very similar to structs, but in this case, we're using interfaces, which will allow for different behaviors, which we couldn't get with structs. So let's use that in our solution. Well, we're going to create a logger struct. We're not going to go back to using our logging target interface because we already have that io.writer interface. So we're going to embed the io.writer interface in our logger struct. Then once again, just for more control over how our logger is created, we're going to create a constructor for our logger, right, the new logger function, and it'll accept a writer. And we're going to return a new logger with that writer as the interface. So we're also going to create the debug method and this debug method will call the right method on the logger. But once again, this isn't the debug method on the logger or it is, but it's also the debug method, the right method on the writer interface that was passed. So putting that all together, we can create a new logger and pass it OS.STD out because OS.STD out implements the io.writer interface. And then when we call the debug method, we're logging to STD out, right? Because that call to write on logger is the same as a call to write on OS.STD out. Now we can also create a file, create a new logger around that file, call the debug method and log to a file. Great, so everything here works. Now this kind of made me question, what else is possible with embedded interfaces? And there are two things that I just went ahead and tried. So the first thing I wondered was, what if I don't pass an implementation of the writer interface? Like right here, I've just created a logger struct. I haven't passed it an implementation of the io.writer interface and I called the debug method. But then I got a runtime error. So it was a panic, it was invalid memory address or nil pointer dereference. And I got a very long stack trace, but eventually the line that was problematic was the call to the write method on the logger. Now, the reason for this is that calling the write method on the logger is actually trying to call the write method on the io.writer implementation that we passed to logger. But since we didn't pass such an implementation, the io.writer is nil or nil because we're in go. And so we're trying to call a method on nil object, which will obviously lead to a nil pointer dereference. But that's okay, because I thought to myself, well, what if I just create my own implementation of the write method, right? So I had logger implement the io.writer interface itself, while also embedding the io.writer interface in the struct. And the write method was very, very straightforward, just call print write print to SDV out. And I put that all together and I created a logger instance and I called the debug method and it printed to SDV out. So we're not dealing with that nil pointer dereference panic that we had earlier. So that's great. This was what I expected to happen, right? So maybe I actually could create a default implementation for a logger, but then I tried it with a file and I created a file and I created a new logger around that file and I called the debug method, but I'm still printing to SDV out. So my conclusion was that I was just overriding any implementation of the write method by the interface we're embedding. So this wasn't very helpful. But say I did still want to create that default implementation, what could I do? Well, first of all, I could create, since I have control over how our logger is created because I created that constructor for our logger, right, the new logger function, well, I can just have the user pass nil as the writer. And if the user passes nil as the writer, I know I need to use the default implementation and the default implementation is to write to SDV out. So in this case, so in this case, if we call the new logger function with nil, we would just be logging to SDV out. And if we call the new logger function with something that wasn't nil, we would be logging to that target. But this solution might be kind of weird because when a user passes nil as the writer, maybe they don't want to log to SDV out. Maybe this behavior is kind of unexpected. So I went ahead and thought of another solution. Well, in this case, we could just create the new logger function and have it return a logger with the default being o and start SDV out as the writer. And then if the user wanted something else that wasn't the default writer, we could just create a set target method on our logger struct and change the writer to be the writer the user requested. So putting that all together, we can very, very easily just create a logger and have it default to SDV out and change it up and log to somewhere else if we wanted to. So both are valid solutions, depends on what the user expects and who your user is going to be and how well you document it, I guess. Now, this isn't the first time I've given this talk and the last time Bill Kennedy was in the crowd, which was amazing. And he came up to me and we started talking about embedding an interface in a struct. And a big question was, is embedding an interface in a struct an anti-pattern? Is that something that shows me that my code isn't the best it could be? And while we were having our conversation, I realized that this isn't the question I should be asking. The question we should be asking is, is this the simplest way to do this? Cause not all cases of embedding an interface in a struct are good cases to embed an interface in a struct, but not all cases where we embed an interface in a struct are all so bad. So this can be useful, but we should be mindful of where we use this. And let's take our logging example and see if the way we embed an interface is good or bad. By asking ourself, is this the simplest way to do this? So, first of all, let's go back a little. Like we realized how embedding structs work, the same thing happens with embedded interfaces, right? So when we embed the IWDR interface in the log struct, it's no different than just creating a writer field of type IO.writer in the log struct and then creating a wrapper function, a wrapper method, the write method, which calls the writer fields write method, right? So now that we know this, well, first of all, we can do this ourselves, right? We can just have the writer field on the log struct. We don't really need that wrapper function because we're only calling the write method a few times and there's no reason not to just call the write method on the writer field directly. So in this case, I really think that what's happening here is the simplest way to do this. Like when I consider if I want to create a field or if I want to embed an interface, I'd probably opt for the field because in this case, we're just using one function from that interface. So it's not up creating a lot of wrapper functions if you wanted to create those wrapper functions and also we're using that interface only internally in our code. So there's no real reason to just go for the interface for the embedding the interface solution. Also embedding an interface might be quite confusing. Not everyone knows what happens when you embed an interface and also when someone calls the write method on the logger, they might expect the implementation to just be of the logger and not to change depending on how the logger was created. So putting that all together, I really think this is the simplest way to do it but there are cases where embedding an interface is in a struct, is useful. So let's take a step back for a second. What changed in our mindset to allow for a new code to work, right? We showed two solutions. We even tweaked our last solution of it and all of these solutions actually worked except for but in contrast to our first solution which had a really hard to detect bug. Well, first of all, we're not extending our logger, right? The way we're thinking of our logger is not to change some default behavior but to inject custom behavior and have the behavior change on depending on how the logger struct was initialized or the global target variable was initialized. Also, we're taking advantage of those features. We Google searched very easily and found the I would not write our interface which made our code so much easier and simpler. So changing our mindset, not thinking about inheritance but this is more of taking the composition side of the inheritance versus composition kind of debate. So taking composition, that's the way Go works. That's the way Go intends you to work with it. We're kind of using dependency ejection, right? We're injecting the outer outer interface when we initialize that logger. So all of these are things you should consider when you're coding in Go. And once again, I said this multiple times but translating code from any language to Go really might cause some problems because we're just talking about different concepts and things that might look very similar aren't really similar behind the surface. So thank you so much for your time. Goodbye.