[MUSIC PLAYING] CAMERON BALAHAN: Hi. I'm Cameron, and today
I'm going to talk about the latest release of
the Go programming language Go 1.18. This is a huge release
that includes new features like fuzzing, workspace mode,
performance improvements, and our biggest change ever
to the language, generics. Supporting generics has been
our most-requested feature since Go 1. In Go 1.18, we've delivered
the generic support for the majority of
our users need today. And there's more coming. In future releases, expect
us to introduce more generics in the Go standard library and
continue to iterate and provide additional support
for some of the more complicated generic use cases. We want your feedback,
which we'll use to learn what we can do better. In fact, that's our approach
to this whole release. We huge changes are
hard to get right. So in Go 1.18, we've laid
the foundation for features that we anticipate we can make
better in the releases to come. Before generics, writing
generic code could be tedious. It often involved
having multiple methods that did exactly the same
thing but with different types of names. Many in the community
heavily relied on Go generate to make
this a little easier. But even then, you
were usually left with huge blocks
of type switches for calling your
generic functions. Other times, libraries
made design decisions about using a type that was a
least common denominator type. For example, take a
look at the map package. Common methods like
Min and Max were forced to operate on a float 64. But what if you wanted to
figure out the Max of two ints? Pre 1.18, you would
have to cast your types to floats to call the functions
and cast the result back to an int after
you get the result. The solution to the
Go generics problem needed to solve two issues. One, it needed a way to
provide parameterized types for strucks functions
and methods. Two, it needed a way
for parameterized types to use common operators,
like equals, less than, and greater than. I would also like to point
out that this example makes use of the currently
experimental constraints package that is being
considered for adoption into the standard library. As you would expect,
generics gives us a way to create parameterized
types and function signatures. Rather than write out multiple
functions for the same behavior on different types, you
can write one function and use it across many types. But this does introduce
some new complexity, which we strive to avoid in Go. Because of this,
the implementation tries to take advantage
of some concepts that most Gophers are
already familiar with. The two most important concepts
are constraints and type sets. Constraints are just interfaces. They can also include a new
concept called type sets. And type sets are the
union of types allowed to be used by the constraint. Next, my colleague Cody
will take a closer look at how generics work in Go. CODY OSS: Thanks, Cameron. As Cameron mentioned,
I am Cody, and I would like to walk you through
how generics, also referred to as parameterized
types, work and how they are different
from implementations in other languages that
you may be familiar with. With the 1.18 release,
Go now supports providing parameterized
types in a couple of different circumstances,
functions, structs, and/or methods. Here, we can see that
this function has one parameterized type
T that is constrained by the constraints ordered. Constraining by
this value not only allows us to pass in different
types like floats and strings into this function,
it also allows us to use the operators
that work on those types, like the less than
operator in this example. Parameterized types also
work on structs and methods. The struct here specifies
one parameterized type T that is constrained to the
types defined by the any, which is equivalent to the empty
interface, meaning, all types. Notice that the
corresponding method we don't need to provide
a constraint for T again as it's already
defined by the struct. Let's break down what
an interface in Go is prior to 1.18. Historically, interfaces
define method sets. Any type that implements
all of the methods defined by an interface implicitly
implements the interface. Let's present that
same information in another way, in relation to
the types and not method sets. Let's say that an
interface defines a set of types, which
are the types that implement the methods
defined by the interface. Any type that is a member
of the interfaces type set implements the interface. In the end, both of
these views of interfaces lead to the same outcome. But this new way of
describing an interface has an advantage that we
can explicitly add types to the set of
interface describes, allowing us to
control the typeset in new and different ways. With Go 1.18, the
syntax for interfaces has been expanded
to optionally allow specifying a set of types. If this type set is
provided, the interface can only be implemented by types
that specified in the type set. As we saw in the
previous example, parameterized values must also
define constraints that scope how the type may be used. In Go, we already have
a concept that can scope how a type can be used. Interfaces, type
constraints are interfaces that may contain some
extra information. All interfaces can, but
not necessarily should, be used as type constraints. The inverse is not
true though, not all things that can
be used as constraints can be used as interfaces. Here's an example of how
constraints package define the sign interface. The sign interface
defines a set of all the built in signed types. The pipe symbol
between the types is used to express that we are
taking the union of the type set. Also note that we
don't need to provide methods for this interface. Since operators like plus,
minus, and less than all work for the types
defined by this interface, any parameterized type that is
constrained by this interface may also use those operators. We don't always want to
restrict an interface to a particular type. Sometimes, we want
the definition to be a little more fuzzy. This is where the new
tilde token comes in. When put in front of a
type, it means the set of all user defined types whose
underlying type is that type. This means that the type
myInt64 in the example also satisfies the
signed interface. Now that we have learned the
concepts behind Go's generics, let's take a quick look
at how they compare to using generics in Java. One of the biggest differences
is that Go does not have type erasure. This means that at
runtime, Go retains all of the type information
associated with the calling function or method. Here we can see, by using
the reflect package, that the type information
of the parameterized type is preserved at runtime. Another difference is
that Go also does not have the idea of wildcards. Since Go is not a traditional
object-oriented language, as it does not have
type hierarchies, there is no need for wildcards. Like the rest of the
language, Go's generics are designed with
simplicity in mind. This means some of the more
complicated features associated with generics in other
languages are not present in Go. These include, no
specialization that allows writing multiple versions
of a generic function designed to work with specific
types, no metaprogramming, allowing code to be
written at compile time to generate code to be
executed at runtime, no contravariance or covariance,
for the same reason Go doesn't support wildcards, no
operator overloading for things like accessing a
map with the bracket syntax. Now that we have learned what
Go generics are and are not, let's take a look and see
where using generics in Go make sense. One example of a
good use case is when you are writing
functions that work with container types
that can be used to process elements of any types. For example, here we
have a function that checks if two slices are equal. To do this operation,
we only need to be able to compare the
elements of the slice. So we use the built-in
constraint comparable. That allows us to do just that. Another case you might
consider using generics is when different types need
to implement some common logic and the implementation
looks the same for all of the different
implementing types. For example, let's
consider the case where we need to sort a slice. The standard library
defines a sort interface that requires three methods,
len, swap, and less. This example shows
the generic struck that implements the methods
for all splice types. This can be done because
the logic for all types is exactly the same. Here, we can see how the
previously-defined slice func type is used to sort any slice
that can provide a comparison function. Here, type parameters
can be used to make it easy for any type to
implement the sort interface. Type parameters worked well
here because the implementation looks the same for all types. We have seen some examples
of where to use generics. Now let's take a look and see
where to not use generics. As discussed earlier,
basic interfaces already provide a
generic programming in Go prior to 1.18. If you were only calling
methods to find on an interface, it is better to not
use type parameters. For example, this function
defines a parameterized type T that is constrained
by io.Reader. io.Reader does not define a
typeset, just to read method. Adding a type parameterization
here just adds complexity and provides no extra value. A function instantiated
with a type argument will most likely not
be any faster than code that uses interface methods. Avoid using generics
in cases like these. Removing the previous
parameterized as type helps make this function
signature more readable. Another place we
should not use generics is if the implementation
is different for each type. In this case, just
use different methods, and don't try to force
generics into your code. For example, the
implementation from reading from a file on disk
and a network socket are quite different. So different methods should
be defined for each operation. Also, there are times
when using reflection might make more sense
than using generics. An example in the
standard library is the encoding JSON package. If we required the
use of an interface, that would require
every type passed in to define a Marshal JSON method. This would be quite
cumbersome, and different types are encoded differently. Generics doesn't make
sense in this case. Now let's take everything
we've learned so far and see how we can
apply what we've learned about generics
to simplify a simple web app written for Go 1.16. Let's take a look at the web app
I have deployed to Cloud Run. It's currently deployed with
the Go 1.16 runtime, which just stopped receiving
security updates with the release of 1.18. So it's time to update
to the latest runtime. The first step is to change the
version declared in the go.mod to 1.18. Next, let's take a
look at our main.go and see what's going
on in our web app. The first thing I
notice right away is that we seem to be
generating some generic types with a tool called genny. Let's take a look at that code. Here, it looks like we're
defining a generic number type that will
later be generated into concrete functions
for each type. The function here is taking the
Max of the elements in a slice. Let's go take a look
at the generated code. It appears that
the two functions are generated for the
two different types that we are working with. This is a great example of a
place we can refactor our code to use generics. So let's do it. I'm going to start
by copy and pasting one of the implementations
into our main.go. This function wants to be
generic over the types int and float 64. So let's first
define a constraint representing those types. I'm going to make use of the
golang/x/exp library, which provides some
common constraints. I run go mod tidy here to
pull in the new dependency. Now let's substitute
out our int type, the generic type T bound
by our number constraint. Now we rename the function
since its general purpose. Next, let's find
the spots that call the old generated
functions and replace them with our new generic function. You will notice that although
I could provide a type, when calling the function,
I don't need to because the compiler is smart
enough to infer the type for us in this instance. Now that we have replaced
the old call sites, we get to do my favorite
part, delete some code. Let's clean up the code that
is no longer being used. It also looks like we no longer
need the dependency on genny. So let's run go mod tidy to
simplify our dependency tree. Sorry, I got a little
excited and jumped straight into refactoring some code. Let's take a step
back and see what else is going on in main.go. Let's start with
the main function. It looks like what's
happening here is we are registering
to HTTP handlers, one that processes ints
and one that processes floats, then we are just
starting up a server. Scrolling down a
little more, it looks like we have a response type
that works with float 64s. Next, we have our
process ints handler. It looks like this decode
some JSON into a slice events, then takes the summoned
Max of that slice, and finally, marshals
the data back to JSON and writes the response
to the calling client. When creating the
response type, it appears we are casting the int
and some Max to a float 64. Maybe instead of this, we can
just make some response generic as well. Let's do that. I again, will make use
of the number constraint we defined earlier. And now we just need to
refactor where the type is used. When instantiating
types, we need to provide the parameterized
type we are generic over. It looks like the sum variable
returned from Sum function is not the right type. Let's take a closer
look at the Sum function to see if we can do some
more refactoring there. It looks like a lot
is going on here. The first thing I noticed
is that the function takes an empty interface, which
is a good first signal that we may be able to refactor it. Next, I notice that we cast the
input parameter to a slice then type switch on the
element type, then it appears we take the sum of
either an int or a float 64. This code can definitely
benefit from a refactor. We will again use the number
constraint we defined earlier and make the function
generic over T. I know that this function
should expect a slice of T. So let's change the input
parameter to match that? Let's also change
the return value to T so that the function
operates only on one type. Now we just clean up the rest. That looks much better. Scrolling back up
to our int handler, it looks like the issue we
saw earlier is now resolved. No more red squiggles. I think we are good with
this handler for now. Let's move on to
the float handler. This appears to be almost
identical to our ints handler above, but it
operates on float 64s. First we decode some JSON,
we process the input, and then marshal the data back
to JSON and write the result. Besides the business
logic and types these two handlers use,
these seem very similar. Let's see if we can do some
more generic refactoring here and make a general HTTP handler. Let's start by copying one
of the existing handlers. We will start refactoring
by making this function return in http handler func. This will let us
turn the function into a middleware we can wrap
our int and float handler with. Now let's figure out
how we can abstract away our business logic. Let's have this middleware be
generic over a function that takes the input I and
returns the output O. Here, I'm making use of the
any constraint, that is a new keyword, and
as mentioned earlier, is just an alias for
the empty interface. Meaning we can pass
any type to it. Next, let's refactor out
the business logic out of the generic handler and
have just those bits in the int and float handlers. In our original handlers, we can
delete a lot of the common code and adjust the input and
output types accordingly. We can actually take
things one step further and reduce this down
to one generic process handler that is constrained
by the number again. Let's see what that
would look like. Finally, we just need to wrap
our tight parameterized generic handler in the
generic middleware. We just did quite a
bit of refactoring in a short amount of time. Let's try starting
up the server locally and testing it with the
little client I wrote. Hey, we did it. It works. I have this workload deployed
to Cloud Run on Google Cloud. So our last step is to ship
it, gcloud, run deploy. And in a few moments, we
would have our new generic app up and running on the Cloud. In this small example, we
were able to reduce our code base by over 40% and get
rid of a dependency, all by adopting generics. I hope that this helped
reinforce some of the ways that adopting generics can
simplify your code base. Back to you, Cameron. CAMERON BALAHAN:
Thank you, Cody. And thank you to
everyone who tuned in. In recap, generics
are a powerful way to simplify your code,
and we hope that you'll use them to be more productive. Tell us what you think. We're eager to
incorporate your feedback to be sure we get it right. Thank you. [MUSIC PLAYING]