Fuzzy generics
CC Attribution 2.0 Belgium:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Palazhchenko, Alexey
Several months ago, a new project was started: FerretDB, an open-source alternative to MongoDB. It is a proxy between MongoDB clients/driver and PostgreSQL, developed in Go. Since the very first commit, it used a version of Go which soon will be released as Go 1.18. The two biggest reasons for that were first-class support for type parameters (generics) and fuzzing. In my talk, I will cover both of them: how they work in theory and our practice, how they were useful for us (spoiler: very useful), and what are their downsides and gotchas.
 Our next speaker will be talking about detecting the lock for shell vulnerability using Go. Sorry, Nova, we're live. What? He's sick? Oh no. We got an erasure. Yeah? What? What's he gonna talk about? Okay, and yeah. Oh, yeah. Yeah, thank you. Thank you. So first of all, I want to wish Leo a very good well soon. Next up, we're gonna talk about two hot topics in Go now. First, we have generics and we also have fuzzing now in Go. What if we just combine them together in one hot new topic talk? Well, Alexi is gonna do that. So let's talk about fuzzing generics. Hello, Fosdum. Thanks for having me. My name is Alexey Palashenko and I will tell you about my experience of using two biggest 1.18 features, generics and fuzzing. Several months ago, we started a new project. It's called FairDB. FairDB is truly open source alternative for MongoDB. It is a proxy written in Go, of course, which speaks MongoDB protocol but stores data in PostgreSQL. This way, you can have open source solution with reliable database and also without all the problems of SSPL. So from the very beginning, we started using 1.18 and you may wonder why because that version is not released yet. And of course, you already know the answer. The two biggest features was generics and fuzzing. And if I show you the relative size of these features, that would be something like that. So we use fuzzing much, much more than generics currently. But let's talk about generics first. We are probably more interested in that. So originally, the very first version of FairDB actually did use generics. And that was several months ago. Generics were not as reliable as they are now. And also, it wasn't very clear if they should be used there or not. So they did not work for us very good. And we switched back to using interfaces as we used interfaces for many, many years ago. And some time ago, I was preparing to talk about how generics work and why they don't work for us. And I was thinking, okay, let me extract small examples just for the talk in the separate repository. So it would be easier to show to people how they work and how they don't. And FairDB is quite a large code base already. So in a small repository, that would be easier to understand. And it started out so they are not that bad, generics. Like when you extract them into a small separate self-contained problem, it becomes much more clear. And also, now we understand that we basically did not use them correctly. So now we know how to do that and maybe we are going to use them in the future. To understand the problem, let me introduce some MongoDB stuff. So you probably heard about Bison. Bison stands for binary JSON, binary serialization format, which is used by MongoDB and also by FairDB. And Bison document is essentially an ordered map, key value pairs, where keys are strings and values, any other Bison value, including other documents, of course. And that's the central data structure in FairDB and also MongoDB. MongoDB commands on protocol level, that's Bison documents. And to know which command is that, we should check the first field of this document. And of course, to have the notion of the first field, it should be ordered map. So how we implement that in Go? Using interfaces, we will do something like that. You probably saw this code many times and wrote it too. So we just define interface with an exported method and then define our own types, which wrap basic types and implement this method, which does nothing. So you may wonder why we have this method, why it's not exported. That's to disallow people to someone else to define types, implement them, this interface in some other packages. So sometimes that's desirable, sometimes not. In that case, you want all Bison types to be defined in the same package, so the users of this package could not define their own Bison types. That sounds reasonable. But also this method does not do anything useful. Also, there are several interesting goals that allow us to check that once we do, for example, switch type, we switch between different types of Bison type, those linters can check that all types are actually checked, all types are covered by cases. And then implementation of documents look like that. We basically have a map of key value pairs and you will see that value is any Bison type, so any value that implement the central phase. And we have a slice of keys, which preserve order. Set method looks as you could expect that to behave. The answer to key is a string and value as Bison type. The check is this value already present in map, if not, we add it into end of keys. So if you add a new key, it's been added to the end in the order. If you set an existing key, it just replaces the value without changing the slice of keys. And we use it like that. So we just call set with string key and value. You can see that key here is not wrapped. That's because that's untyped string constant. And untyped string constant can be automatically converted to string and any string like type. And now string type is very much like string. But for value, we have to wrap it into Bison type interface or int implementation interface. So here we have to wrap it explicitly. The problem here is that we have to wrap it. It would be nice to have to use basic types for both key and value, right? Currently it's not possible with interfaces. Another problem we can also set a new value. Sometimes that could be desirable. For example, in Bison actually there is a new value. It can be represented with new. But we decided to use a separate value for now because new causes too much problems. So it would be nice to just allow that code in compile time to prevent someone essentially by passing new to their function. So let's see how the same code will look in genetics. So first of all, we define constraint, which looks like an interface, but have a list of methods inside. And you can see that this constraint can be is implemented, you can say by int, string or pointer to document. It looks like a kind of like a type union, but it's not. And that's important. And here is a document. And you can see that map value here is any. Any is an empty interface in Govo.18. So instead of interface and empty square brackets, curly brackets, you can say any. But why we don't use Bison type here? Why we introduced all those generics to have compile time safety, but here we use interface which says even less than Bison type on the previous example. The reason for that is that Bison type is constrained, not just an interface. At one particular moment, that could be only one specific type. For example, I can create a document which contains only integers as values or only strings as a value. But if you want to have documents that contains both of them, I can't use any constraints there. So I have to use any. In fact, that's not that bad because that's implementation detail and we can expose all the public functionality of the document type as methods with actual functions with constraint. So inside it will be wrapped into interface, but all outside API will be type checked as a compile time. Okay, let's see how the method for certain value would look. It looks like almost exactly the same as that method in the previous example. But here we accept additional type parameter of type Bison type and we call it T and then we use it in the actual parameter list. So key is a string, now it's a basic type string, not our own type and value of type T. So the rest of the implementation looks the same. So that code actually has one problem. It doesn't compile. And that's because methods cannot have type parameters yet in Go and for that reason it's quite interesting. So by definition, interface is just a set of methods and if you type to implement some interface, you can do that by just defining the same methods as an interface. But what if interface contains methods with type parameters? Should you, like, how a type can implement this interface by using the same type parameters or any type parameters, which are compatible or not? How that should work? So as I said, methods are just syntax-sugar for functions and Go team for now decided not to have methods solved because it is not clear if interfaces can be implemented by method with type parameters or not. Should be method with type parameters be a part of interfaces at all or not. So for now they decided that functions are enough and maybe in the future methods could have type parameters. So we have to use functions. Function looks exactly the same as the previous example, but now we have to pass document as the first argument instead of passing it as a receiver. As in that, we still pass the type parameter and we still use the type parameter as a way. So how the usage looks like. Here we call documentSetFunction passing document and who is this basic string and then values a basic integer. Great. And then second example does not compile because nil is not a part of our BisonType constate. That doesn't compile. Also. So let's take a look at that function. That function creates a new document. It accepts a number of pairs. You probably see this API pattern somewhere. And then every time this function should check the number of parameters passed, it should check the number of parameters is right. The pairs not an incorrect number of arguments which do not form a pair. It should check type of arguments, etc. etc. All that done is in runtime. It would be nice to have it to check at compile time, but that's not possible with interfaces. So how we use it. We call a number of functions with number of parameters and again, all these values are wrapped, but we can't do better with interfaces. It is also possible to call this function with invalid number of arguments and also with invalid types. So the key of the document should be string, but here it is integer. Both those cases are not covered at compile time. So how can we do that better with generics? With generics, we can write this function, which accepts a single type parameter of value of this field and one key value of part. It's a return of the document. Here is not possible there, so we don't even return that because invalid number of parameters is not possible to pass, it is not possible to pass nil there. It is also not possible to pass invalid key time. But what if we want to make a document with two pairs? Functionally, there is no yet support for very addictive type arguments, so we have to write something like that. We pass two type parameters and then we create two key value pairs. And if we want to want to create documents with three pairs, we have to use function like that. Of course, that doesn't look so good, but that's the best we can do right now, unfortunately. Okay, let's take a look at getMethod. So for interfaces, that's very, very basic method that should just return the value from the map. And if that map value is not present, the nil is return. Nil is default zero value for all interfaces. So here it is actually somewhat useful. And also, we have to check that the value has the same type. So we can't check just this basic int, we have to wrap our int into interface. So how that looks in generics. So let's write this function, which returns the value, and here we say that okay, the user should not only use a key, but also shouldn't know the type of the field, right? So we convert this return value to the given type. And if the type is not correct, we return a zero value of the type to indicate that this field is not present, or type is incorrect. We can return also bullial value, for example, but for simplicity, let's say return zero value. That may be not that useful. Again, the problem here is that the code should know the type. Here we don't assign any existing variables, so we have to specify type explicitly. So I don't know how much that useful. In my practice, interfaces are better in some aspects, what's in others, and generics, of course, the same. Sometimes they solve the problem fast, sometimes they don't. So if you're looking for an easy answer for you, should you use generics or interfaces? Unfortunately, I don't have it. I would say it depends on your situation, but at least now you know the limitations. There are no methods, and then there are problems with what types you can use and what already works and what not. Okay, let's look into finding. Finding might be much more simply and more interesting. So what is finding? Finding essentially is a testing quick on steroids, and testing quick is a standard go package that allows you to generate random values. And finding kind of works like that, it generates random values and pass it to your function. But then it tries to generate good values that increase the chance of your code to hunk or crash or panic or take an infinite loop or use a lot of memory, something like that. And it implements it in a way somewhat similar in theory, not in implementation, but in principle, how test coverage works. So it instruments the code and see what code is being executed with what input, and tries to generate better input to increase test coverage. And one important part of RGB is MongoDB binary protocol parsing packages, of course, and that's the sweetest spot for fuzzing because binary parsing is quite complicated. There are a lot of HK cases, a lot of problems you could not just find with basic unit tests because we developers try to test for happy path and fuzzing can generate inputs that make our parser crash. So actually, FurnitureDB had fuzzing tests even before unit tests. They were as that useful. That's because we started by dumping MongoDB protocol packages and writing code to parse them. And we were more interested in getting this right like not having any crashes. And after that, once we started to understand protocol, we started writing good unit tests, not just fuzzing tests. But before we talk about fuzzing, let's talk a bit about test-diving tests. So you probably know what that we just defined a test case with the name, with some bytes, expected value, and expected error. And we check it like that. We unmarch all bytes into value and compare that we got the same value by unmarching these bytes. And then we march all v or v2. It doesn't matter because they're the same. To the new slice of bytes and compare that bytes are the same. So that's very basic test for parsing binary data. Quite common. And it looks like that. We just iterate over test cases and run our subtest with a name. And name allows us to have nice error message and also do not fail for the first test case, but for each test case that fails individually. So fuzzing looks very similar. We write a fast document function which accepts testing f instead of testing t. And you just call f at with test cases with generated bytes, with bytes from test case. And there is a fast function similar to subtest function, but fast function which accepts additional parameter b. And b is generated bytes. So then fuzzing starts to generate interesting value based on already provided seed values. And seed values is what you pass to f at. In my case, that data from table test. And that's also, I think, would be quite common, you tell me in the future. And then we do the same. We unmarch all bytes, compare, then match all them and compare. So what are problems with that approach? First of all, we can unmarch all data, then match all them back and get different values for bytes. How is that? So first of all, the first slice of bytes may be larger than we needed. So that happens when, for example, you unmarch all some value from a slice of bytes, but some amount of bytes is not read during the unmatching process. Let's say you, for example, unmarch all JSON document, and the first byte you've got is curly bracket and second byte is closing curly bracket. So you just read two bytes and you got the full JSON document. And then the rest of bytes generated. So solution there is quite simple. You just, an read portion of B should be reward. Once you compare B with B2, on the next step, you compare with the slice of bytes which was actually read. Another problem that sometimes two different combinations of bytes produce the same result. Again, in the case of JSON, that could be just additional bytes space. For example, you have a document and you have a white space between colon and the field value. And there are no difference from the logic perspective, but bytes on the wire in the memory could be different. And solution for that to use some form of canonization. For example, for JSON, you just call JSONCompact and get the canonical version of that representation. Okay, so what are current issues with filing? First of all, there is no support for subtest, unfortunately. You can't call testing F run because it does not exist. In that case, that we can't use the same function for both table-deriving tests and filing tests. And they are very similar. In the previous example, you saw that they look very, very similar. It would be nice to be able to iterate over test cases, test them right there, and then pass to filing function, right? But there is no support for subtest, and that means that first test case that fails actually fails the whole test. That's not very helpful. Hopefully, some form of subtest can be introduced in the future. Second problem is that seed values are unnamed. And that means that once we add seed values to the corpus, and then we call the first function on them, and one of them fails, the fail message would say something like, okay, this function fails for seed value number two. And you have to check all the seed values. And if you are low on the seed values from the files on disk, for example, not from the code, you have to figure out which seed value was that. It's good if files have distinct names like one to three, but if there's a hash, it could be tricky. And I encountered a strange problem, which is not very easily to produce, but very easy for me, but I wasn't able to reduce it to something small, so I contained back it up. So one thing that Fizing should detect is the hanging test. But in my case, if I call the Fizing with gomax.proxonset, so by default it uses all the cores, just like compilation or running normal tests to use all the cores of your processor. Sometimes Fizing fails with error message saying, okay, for that particular test case, the function hunts. And it writes a file on disk and tells you, okay, run this command, go test with this flag to check this particular input. And you run it and it passes. And the only workaround I found that said gomax.prox mainly to some smaller values than the number of cores you have. So for example, for 20-core machine, I pass six and six walks reliably. If I pass 10, sometimes not. So by default, I set it to twice as little as cores as I have. I will try to produce a problem in the report, maybe it will be fixed before release. But overall, Fizing is great. It found, by using it, I found a few problems in go test class itself and most of them were fixed and also I found many, many, many bugs in F and here are some links. I created a separate repository to extract all the information from this talk about generics versus interfaces. There is another repository I want to give a shout out. It's like, it's a tradition to generics starting from the very, very beginning to the internals and implementation details. There are two official tutorials for Fizing and for generics now on the website. And there is a separate page for Fizing specifically. And there are links about me and FerDb project. Go check out us on GitHub, give us a star and also you can subscribe to me on GitHub and Twitter. Thank you. So, it's actually one of the issues I found during my usage of Fizing. It wasn't possible in one edge case and I reported the problem to a fixer now in the latest version of GoFU compiled from the source. So, I'm going to run Fizing with Resetector because it's useful to find the races in Fizing functions. Thank you. A question from me. Would you recommend everyone to use Fizing or only in certain use cases? So, there are cases when Fizing works like very, very good like anything for untrusted input, even for trusted input for binary data for text protocols. Fizing is a must and I would say that for me, that is already quite a big right flag if I go check some, I would please do it on GitHub, say, just maybe yesterday I found a new project which is like a virtual machine written on Go, but there are no Fizing tests. And that was a big right flag for me and I'm looking forward to contribute to Fizing test support. There are other cases where Fizing is not easy. So, you can in theory use Fizing to generate validity scroll queries and then to the database. Other approaches could be simpler, but also if you have a really powerful Fizing engine like in Go, you can also try to use that. Where are any challenges, Will asks, when upgrading Farity B to Go118 before the official release? So, there were no challenges because we started with the truly section. Only for that, but also, I know from the very beginning that once we go production ready, Go118 will be released already, so that wasn't a problem for us. And Fizing for me was that important to use that version. How long, the question has just moved on my screen, that's annoying. How long does it take to go Fizing APIs so that you can start to be productive and write integrating tests? So, I would say that API is the best example of how Fizing works in Go, how it is integrated. So, before that, there was a separate project that requires separate tooling, separate run instructions, separate way. Now it is very deeply integrated with Go test. So, you just go test fast and fast function name and the trust. So, something is very, very easy. And being productive and getting bugs from Fizing is also very easy. And this time you start writing better and better Fizing functions you like. And now you can, for example, check two different applications and compare them and find bugs this way and so on. So, writing good fast functions takes some time, but starting is very easy. I recommend everyone to do that. MFRW asks, from what I've read, Fizing is a process that goes on, deal the trumpet blows, where do we draw the line while first test and keeping track of time of resources that Fizing actually takes? Yeah, so that's a good question. So, first of all, Fizing can find like, there are two kinds of issues Fizing can find, right? One of them is just bugs, findings, errors, and so on. But the second type of issues is Fizing finds something which takes a lot of memory or takes a lot of time. And, for example, you can fast something like virtual machine again, virtual machine can just execute a program of a lead program which just happens to contain an infinite loop. There is no easy answer for that. For example, in the virtual machine example, you can add some facility to cancel the execution and then call this facility and check that it actually works, that your virtual machine is not hung completely. And then there is another aspect of that is how often you run Fizing, do you run it on CI, do you run it on your machine and your garage for hours at time? Again, there is no easy answer for that. My take would be like run it on CI for a few minutes for each request you receive. So basically, one other job run, integration test, the interest and so on, the other job could run Fizing. Then have maybe daily Fizing for an hour or something like that. That's what we do, at least for now. I have a follow-up question. I say, sorry, there are any technical delays. My airport just has mid-question died on me, so I quickly changed the backup. Siegmund asks, how do you run them in a CI and more specifically, why do you post your results of fast tests? Because they are inconsistent. Yeah, so great question. Right now, Fizing has two corpuses, one of them is called Sieg corpus and it stores data in Go code you added by using add functions. And also, they test data directly with Fizing. And the other corpus is generated corpus and it's started in Go cache. And in Farydb, we tried an experimental approach, we basically combined two corpuses together and store them in separate repository and fetches repository on CI and then push updates from CI to the repository. You can find it on GitHub in Farydb organizations, fast corpus repository. That's an experiment. I don't know if it would be a best practice for us, fast, go forward a lot. Thank you. If there's still any questions, we still have around three minutes left, so please feel free to post them in the chat. One more question is, did you notice any performance impacts when using generics in your code? So we are quite far from that yet. We are working on basic functionality like making it work and we did not start doing performance testing yet. There are a lot of places where performance of Farydb can be improved. In some cases it was optimized for simplicity and in most cases it was optimized for the speed of the writing code. So there are a lot of not good code, not performance code and once they start working on that, we will investigate. But also I hope at that time generics will be faster and not a problem fast yet. Thank you. And I'm actually getting more compliments than questions right now in the chat. So I think everyone answered all the questions about generics for today. So unless there was any quick last question, I really want to thank you again. Alexi just jumped in for the last week to produce this talk because we had a Leo Wasek. So thank you very much for doing this all last minute. Thank you for joining us again. And I think he also deserves a round of applause. I wish you many luck on the podcast you have to go on and the five same talks you have to do still today. Thank you. Thank you very much. Bye.
 Our next speaker will be talking about detecting the lock for shell vulnerability using Go.  Sorry, Nova, we're live.  What?  He's sick?  Oh no.  We got an erasure.  Yeah?  What?  What's he gonna talk about?  Okay, and yeah.  Oh, yeah.  Yeah, thank you.  Thank you.  So first of all, I want to wish Leo a very good well soon.  Next up, we're gonna talk about two hot topics in Go now.  First, we have generics and we also have fuzzing now in Go.  What if we just combine them together in one hot new topic talk?  Well, Alexi is gonna do that.  So let's talk about fuzzing generics.  Hello, Fosdum.  Thanks for having me.  My name is Alexey Palashenko and I will tell you about my experience of using two biggest  1.18 features, generics and fuzzing.  Several months ago, we started a new project.  It's called FairDB.  FairDB is truly open source alternative for MongoDB.  It is a proxy written in Go, of course, which speaks MongoDB protocol but stores data in  PostgreSQL.  This way, you can have open source solution with reliable database and also without all  the problems of SSPL.  So from the very beginning, we started using 1.18 and you may wonder why because that version  is not released yet.  And of course, you already know the answer.  The two biggest features was generics and fuzzing.  And if I show you the relative size of these features, that would be something like that.  So we use fuzzing much, much more than generics currently.  But let's talk about generics first.  We are probably more interested in that.  So originally, the very first version of FairDB actually did use generics.  And that was several months ago.  Generics were not as reliable as they are now.  And also, it wasn't very clear if they should be used there or not.  So they did not work for us very good.  And we switched back to using interfaces as we used interfaces for many, many years  ago.  And some time ago, I was preparing to talk about how generics work and why they don't  work for us.  And I was thinking, okay, let me extract small examples just for the talk in the separate  repository.  So it would be easier to show to people how they work and how they don't.  And FairDB is quite a large code base already.  So in a small repository, that would be easier to understand.  And it started out so they are not that bad, generics.  Like when you extract them into a small separate self-contained problem, it becomes much more  clear.  And also, now we understand that we basically did not use them correctly.  So now we know how to do that and maybe we are going to use them in the future.  To understand the problem, let me introduce some MongoDB stuff.  So you probably heard about Bison.  Bison stands for binary JSON, binary serialization format, which is used by MongoDB and also  by FairDB.  And Bison document is essentially an ordered map, key value pairs, where keys are strings  and values, any other Bison value, including other documents, of course.  And that's the central data structure in FairDB and also MongoDB.  MongoDB commands on protocol level, that's Bison documents.  And to know which command is that, we should check the first field of this document.  And of course, to have the notion of the first field, it should be ordered map.  So how we implement that in Go?  Using interfaces, we will do something like that.  You probably saw this code many times and wrote it too.  So we just define interface with an exported method and then define our own types, which  wrap basic types and implement this method, which does nothing.  So you may wonder why we have this method, why it's not exported.  That's to disallow people to someone else to define types, implement them, this interface  in some other packages.  So sometimes that's desirable, sometimes not.  In that case, you want all Bison types to be defined in the same package, so the users  of this package could not define their own Bison types.  That sounds reasonable.  But also this method does not do anything useful.  Also, there are several interesting goals that allow us to check that once we do, for example,  switch type, we switch between different types of Bison type, those linters can check that  all types are actually checked, all types are covered by cases.  And then implementation of documents look like that.  We basically have a map of key value pairs and you will see that value is any Bison type,  so any value that implement the central phase.  And we have a slice of keys, which preserve order.  Set method looks as you could expect that to behave.  The answer to key is a string and value as Bison type.  The check is this value already present in map, if not, we add it into end of keys.  So if you add a new key, it's been added to the end in the order.  If you set an existing key, it just replaces the value without changing the slice of keys.  And we use it like that.  So we just call set with string key and value.  You can see that key here is not wrapped.  That's because that's untyped string constant.  And untyped string constant can be automatically converted to string and any string like type.  And now string type is very much like string.  But for value, we have to wrap it into Bison type interface or int implementation interface.  So here we have to wrap it explicitly.  The problem here is that we have to wrap it.  It would be nice to have to use basic types for both key and value, right?  Currently it's not possible with interfaces.  Another problem we can also set a new value.  Sometimes that could be desirable.  For example, in Bison actually there is a new value.  It can be represented with new.  But we decided to use a separate value for now because new causes too much problems.  So it would be nice to just allow that code in compile time to prevent someone  essentially by passing new to their function.  So let's see how the same code will look in genetics.  So first of all, we define constraint, which looks like an interface,  but have a list of methods inside.  And you can see that this constraint can be is implemented,  you can say by int, string or pointer to document.  It looks like a kind of like a type union, but it's not.  And that's important.  And here is a document.  And you can see that map value here is any.  Any is an empty interface in Govo.18.  So instead of interface and empty square brackets,  curly brackets, you can say any.  But why we don't use Bison type here?  Why we introduced all those generics to have compile time safety,  but here we use interface which says even less than Bison type on the previous example.  The reason for that is that Bison type is constrained, not just an interface.  At one particular moment, that could be only one specific type.  For example, I can create a document which contains only integers as values  or only strings as a value.  But if you want to have documents that contains both of them,  I can't use any constraints there.  So I have to use any.  In fact, that's not that bad because that's implementation detail  and we can expose all the public functionality of the document type  as methods with actual functions with constraint.  So inside it will be wrapped into interface,  but all outside API will be type checked as a compile time.  Okay, let's see how the method for certain value would look.  It looks like almost exactly the same as that method in the previous example.  But here we accept additional type parameter of type Bison type  and we call it T and then we use it in the actual parameter list.  So key is a string, now it's a basic type string, not our own type and value of type T.  So the rest of the implementation looks the same.  So that code actually has one problem.  It doesn't compile.  And that's because methods cannot have type parameters yet in Go  and for that reason it's quite interesting.  So by definition, interface is just a set of methods  and if you type to implement some interface,  you can do that by just defining the same methods as an interface.  But what if interface contains methods with type parameters?  Should you, like, how a type can implement this interface  by using the same type parameters or any type parameters,  which are compatible or not? How that should work?  So as I said, methods are just syntax-sugar for functions  and Go team for now decided not to have methods solved  because it is not clear if interfaces can be implemented by method with type parameters or not.  Should be method with type parameters be a part of interfaces at all or not.  So for now they decided that functions are enough  and maybe in the future methods could have type parameters.  So we have to use functions.  Function looks exactly the same as the previous example,  but now we have to pass document as the first argument  instead of passing it as a receiver.  As in that, we still pass the type parameter  and we still use the type parameter as a way.  So how the usage looks like.  Here we call documentSetFunction passing document  and who is this basic string and then values a basic integer. Great.  And then second example does not compile  because nil is not a part of our BisonType constate.  That doesn't compile. Also.  So let's take a look at that function.  That function creates a new document.  It accepts a number of pairs.  You probably see this API pattern somewhere.  And then every time this function should check the number of parameters passed,  it should check the number of parameters is right.  The pairs not an incorrect number of arguments which do not form a pair.  It should check type of arguments, etc. etc.  All that done is in runtime.  It would be nice to have it to check at compile time,  but that's not possible with interfaces.  So how we use it.  We call a number of functions with number of parameters  and again, all these values are wrapped,  but we can't do better with interfaces.  It is also possible to call this function with invalid number of arguments  and also with invalid types.  So the key of the document should be string, but here it is integer.  Both those cases are not covered at compile time.  So how can we do that better with generics?  With generics, we can write this function,  which accepts a single type parameter of value of this field  and one key value of part.  It's a return of the document.  Here is not possible there, so we don't even return that  because invalid number of parameters is not possible to pass,  it is not possible to pass nil there.  It is also not possible to pass invalid key time.  But what if we want to make a document with two pairs?  Functionally, there is no yet support for very addictive type arguments,  so we have to write something like that.  We pass two type parameters and then we create two key value pairs.  And if we want to want to create documents with three pairs,  we have to use function like that.  Of course, that doesn't look so good,  but that's the best we can do right now, unfortunately.  Okay, let's take a look at getMethod.  So for interfaces, that's very, very basic method  that should just return the value from the map.  And if that map value is not present,  the nil is return.  Nil is default zero value for all interfaces.  So here it is actually somewhat useful.  And also, we have to check that the value has the same type.  So we can't check just this basic int,  we have to wrap our int into interface.  So how that looks in generics.  So let's write this function,  which returns the value,  and here we say that okay,  the user should not only use a key,  but also shouldn't know the type of the field, right?  So we convert this return value to the given type.  And if the type is not correct,  we return a zero value of the type  to indicate that this field is not present,  or type is incorrect.  We can return also bullial value, for example,  but for simplicity, let's say return zero value.  That may be not that useful.  Again, the problem here is that the code should know the type.  Here we don't assign any existing variables,  so we have to specify type explicitly.  So I don't know how much that useful.  In my practice, interfaces are better in some aspects,  what's in others, and generics, of course, the same.  Sometimes they solve the problem fast, sometimes they don't.  So if you're looking for an easy answer for you,  should you use generics or interfaces?  Unfortunately, I don't have it.  I would say it depends on your situation,  but at least now you know the limitations.  There are no methods, and then there are problems  with what types you can use  and what already works and what not.  Okay, let's look into finding.  Finding might be much more simply and more interesting.  So what is finding?  Finding essentially is a testing quick on steroids,  and testing quick is a standard go package  that allows you to generate random values.  And finding kind of works like that,  it generates random values and pass it to your function.  But then it tries to generate good values  that increase the chance of your code to hunk or crash or panic  or take an infinite loop or use a lot of memory,  something like that.  And it implements it in a way somewhat similar in theory,  not in implementation, but in principle, how test coverage works.  So it instruments the code and see what code is being executed  with what input, and tries to generate better input  to increase test coverage.  And one important part of RGB is MongoDB binary protocol  parsing packages, of course, and that's the sweetest spot  for fuzzing because binary parsing is quite complicated.  There are a lot of HK cases, a lot of problems  you could not just find with basic unit tests  because we developers try to test for happy path  and fuzzing can generate inputs that make our parser crash.  So actually, FurnitureDB had fuzzing tests  even before unit tests.  They were as that useful.  That's because we started by dumping MongoDB protocol packages  and writing code to parse them.  And we were more interested in getting this right  like not having any crashes.  And after that, once we started to understand protocol,  we started writing good unit tests, not just fuzzing tests.  But before we talk about fuzzing,  let's talk a bit about test-diving tests.  So you probably know what that we just defined a test case  with the name, with some bytes, expected value,  and expected error.  And we check it like that.  We unmarch all bytes into value  and compare that we got the same value  by unmarching these bytes.  And then we march all v or v2.  It doesn't matter because they're the same.  To the new slice of bytes and compare that bytes are the same.  So that's very basic test for parsing binary data.  Quite common.  And it looks like that.  We just iterate over test cases and run our subtest with a name.  And name allows us to have nice error message  and also do not fail for the first test case,  but for each test case that fails individually.  So fuzzing looks very similar.  We write a fast document function  which accepts testing f instead of testing t.  And you just call f at with test cases with generated bytes,  with bytes from test case.  And there is a fast function similar to subtest function,  but fast function which accepts additional parameter b.  And b is generated bytes.  So then fuzzing starts to generate interesting value  based on already provided seed values.  And seed values is what you pass to f at.  In my case, that data from table test.  And that's also, I think, would be quite common,  you tell me in the future.  And then we do the same.  We unmarch all bytes, compare, then match all them and compare.  So what are problems with that approach?  First of all, we can unmarch all data,  then match all them back and get different values for bytes.  How is that?  So first of all, the first slice of bytes may be larger  than we needed.  So that happens when, for example,  you unmarch all some value from a slice of bytes,  but some amount of bytes is not read during the unmatching process.  Let's say you, for example, unmarch all JSON document,  and the first byte you've got is curly bracket  and second byte is closing curly bracket.  So you just read two bytes and you got the full JSON document.  And then the rest of bytes generated.  So solution there is quite simple.  You just, an read portion of B should be reward.  Once you compare B with B2, on the next step,  you compare with the slice of bytes which was actually read.  Another problem that sometimes two different combinations  of bytes produce the same result.  Again, in the case of JSON, that could be just additional bytes space.  For example, you have a document  and you have a white space between colon and the field value.  And there are no difference from the logic perspective,  but bytes on the wire in the memory could be different.  And solution for that to use some form of canonization.  For example, for JSON, you just call JSONCompact  and get the canonical version of that representation.  Okay, so what are current issues with filing?  First of all, there is no support for subtest, unfortunately.  You can't call testing F run because it does not exist.  In that case, that we can't use the same function  for both table-deriving tests and filing tests.  And they are very similar.  In the previous example, you saw that they look very, very similar.  It would be nice to be able to iterate over test cases,  test them right there, and then pass to filing function, right?  But there is no support for subtest, and that means that  first test case that fails actually fails the whole test.  That's not very helpful.  Hopefully, some form of subtest can be introduced in the future.  Second problem is that seed values are unnamed.  And that means that once we add seed values to the corpus,  and then we call the first function on them,  and one of them fails,  the fail message would say something like,  okay, this function fails for seed value number two.  And you have to check all the seed values.  And if you are low on the seed values from the files on disk,  for example, not from the code,  you have to figure out which seed value was that.  It's good if files have distinct names like one to three,  but if there's a hash, it could be tricky.  And I encountered a strange problem,  which is not very easily to produce, but very easy for me,  but I wasn't able to reduce it to something small,  so I contained back it up.  So one thing that Fizing should detect is the hanging test.  But in my case, if I call the Fizing with gomax.proxonset,  so by default it uses all the cores,  just like compilation or running normal tests  to use all the cores of your processor.  Sometimes Fizing fails with error message saying,  okay, for that particular test case, the function hunts.  And it writes a file on disk and tells you,  okay, run this command, go test with this flag  to check this particular input.  And you run it and it passes.  And the only workaround I found that said gomax.prox  mainly to some smaller values than the number of cores you have.  So for example, for 20-core machine,  I pass six and six walks reliably.  If I pass 10, sometimes not.  So by default, I set it to twice as little as cores as I have.  I will try to produce a problem in the report,  maybe it will be fixed before release.  But overall, Fizing is great.  It found, by using it, I found a few problems in go test  class itself and most of them were fixed and also I found many,  many, many bugs in F  and here are some links.  I created a separate repository to extract all the information  from this talk about generics versus interfaces.  There is another repository I want to give a shout out.  It's like, it's a tradition to generics starting from the very,  very beginning to the internals and implementation details.  There are two official tutorials for Fizing  and for generics now on the website.  And there is a separate page for Fizing specifically.  And there are links about me and FerDb project.  Go check out us on GitHub, give us a star and also you can  subscribe to me on GitHub and Twitter.  Thank you.  So, it's actually one of the issues I found during my usage of Fizing.  It wasn't possible in one edge case and I reported the problem to a fixer  now in the latest version of GoFU compiled from the source.  So, I'm going to run Fizing with Resetector because it's useful to find the races in Fizing functions.  Thank you.  A question from me.  Would you recommend everyone to use Fizing or only in certain use cases?  So, there are cases when Fizing works like very, very good like anything for  untrusted input, even for trusted input for binary data for text protocols.  Fizing is a must and I would say that for me, that is already quite a big  right flag if I go check some, I would please do it on GitHub, say, just maybe  yesterday I found a new project which is like a virtual machine written on Go,  but there are no Fizing tests.  And that was a big right flag for me and I'm looking forward to contribute to Fizing test support.  There are other cases where Fizing is not easy.  So, you can in theory use Fizing to generate validity scroll queries and then to the database.  Other approaches could be simpler, but also if you have a really powerful Fizing engine  like in Go, you can also try to use that.  Where are any challenges, Will asks, when upgrading Farity B to Go118 before the official release?  So, there were no challenges because we started with the truly section.  Only for that, but also, I know from the very beginning that once we go production  ready, Go118 will be released already, so that wasn't a problem for us.  And Fizing for me was that important to use that version.  How long, the question has just moved on my screen, that's annoying.  How long does it take to go Fizing APIs so that you can start to be productive and write integrating tests?  So, I would say that API is the best example of how Fizing works in Go, how it is integrated.  So, before that, there was a separate project that requires separate tooling, separate run instructions, separate way.  Now it is very deeply integrated with Go test. So, you just go test fast and fast function name and the trust.  So, something is very, very easy.  And being productive and getting bugs from Fizing is also very easy.  And this time you start writing better and better Fizing functions you like.  And now you can, for example, check two different applications and compare them and find bugs this way and so on.  So, writing good fast functions takes some time, but starting is very easy. I recommend everyone to do that.  MFRW asks, from what I've read, Fizing is a process that goes on, deal the trumpet blows, where do we draw the line while first test and  keeping track of time of resources that Fizing actually takes?  Yeah, so that's a good question.  So, first of all, Fizing can find like, there are two kinds of issues Fizing can find, right?  One of them is just bugs, findings, errors, and so on.  But the second type of issues is Fizing finds something which takes a lot of memory or takes a lot of time.  And, for example, you can fast something like virtual machine again, virtual machine can just execute a program of a lead program which just happens to contain an infinite loop.  There is no easy answer for that.  For example, in the virtual machine example, you can add some facility to cancel the execution and then call this facility and check that it actually works,  that your virtual machine is not hung completely.  And then there is another aspect of that is how often you run Fizing, do you run it on CI, do you run it on your machine and your garage for hours at time?  Again, there is no easy answer for that.  My take would be like run it on CI for a few minutes for each request you receive.  So basically, one other job run, integration test, the interest and so on, the other job could run Fizing.  Then have maybe daily Fizing for an hour or something like that.  That's what we do, at least for now.  I have a follow-up question.  I say, sorry, there are any technical delays.  My airport just has mid-question died on me, so I quickly changed the backup.  Siegmund asks, how do you run them in a CI and more specifically, why do you post your results of fast tests?  Because they are inconsistent.  Yeah, so great question.  Right now, Fizing has two corpuses, one of them is called Sieg corpus and it stores data in Go code you added by using add functions.  And also, they test data directly with Fizing.  And the other corpus is generated corpus and it's started in Go cache.  And in Farydb, we tried an experimental approach, we basically combined two corpuses together and store them in separate repository and fetches repository on CI and then push updates from CI to the repository.  You can find it on GitHub in Farydb organizations, fast corpus repository.  That's an experiment. I don't know if it would be a best practice for us, fast, go forward a lot.  Thank you. If there's still any questions, we still have around three minutes left, so please feel free to post them in the chat.  One more question is, did you notice any performance impacts when using generics in your code?  So we are quite far from that yet. We are working on basic functionality like making it work and we did not start doing performance testing yet.  There are a lot of places where performance of Farydb can be improved.  In some cases it was optimized for simplicity and in most cases it was optimized for the speed of the writing code.  So there are a lot of not good code, not performance code and once they start working on that, we will investigate.  But also I hope at that time generics will be faster and not a problem fast yet.  Thank you.  And I'm actually getting more compliments than questions right now in the chat.  So I think everyone answered all the questions about generics for today.  So unless there was any quick last question, I really want to thank you again.  Alexi just jumped in for the last week to produce this talk because we had a Leo Wasek.  So thank you very much for doing this all last minute. Thank you for joining us again. And I think he also deserves a round of applause.  I wish you many luck on the podcast you have to go on and the five same talks you have to do still today.  Thank you. Thank you very much.  Bye.
go_log4shell_0_1583.png go_log4shell_1584_2387.png go_log4shell_2388_2627.png go_log4shell_2628_3059.png go_log4shell_3060_3407.png go_log4shell_3408_3647.png go_log4shell_3648_6635.png go_log4shell_6636_7235.png go_log4shell_7236_14159.png go_log4shell_14160_21731.png go_log4shell_21732_22163.png go_log4shell_22164_22667.png go_log4shell_22668_23195.png go_log4shell_23196_23375.png go_log4shell_23376_30551.png go_log4shell_30552_30659.png go_log4shell_30660_31343.png go_log4shell_31344_36599.png go_log4shell_36600_37271.png go_log4shell_37272_37403.png go_log4shell_37404_38915.png go_log4shell_38916_39863.png go_log4shell_39864_41063.png go_log4shell_41064_41855.png go_log4shell_41856_43847.png go_log4shell_43848_43979.png go_log4shell_43980_44063.png go_log4shell_44064_45695.png go_log4shell_45696_46067.png go_log4shell_46068_47435.png go_log4shell_47436_48023.png go_log4shell_48024_48185.png