Introduction to Go, part 15: Channels

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello and welcome to the final video in this series introducing the go language over the course of this video series we talked about a lot of structures and techniques and the tools that are available in order to get started successfully programming with the go language well I want to wrap up that discussion in this video by talking about one of the features that makes go really stand out when you're looking for different languages to work with and that is this concept of channels now most programming languages that are out there were originally designed with a single processing core in mind and so when concurrency and parallelism came into play they were really kind of bolted on to the side and so a lot of times you're actually going to be working with third-party libraries and packages in order to help with data synchronization and things like that well NGO was born in a multiprocessor world so every computer that was out there when go was invented had more than one processing core so it made sense as the language was being designed to consider concurrency and parallelism from the beginning now the last video we talked about go routines and how go abstracts the concept of a thread into this higher concept called a go routine to allow hundreds or thousands or even tens of thousands of things to be going on in your application at the same time well in this video we're going to be talking about channels and how those can be used to pass data between different go routines in a way that is safe and prevents issues such as race conditions and memory sharing problems that can cause issues in your application that are very difficult to debug so we're going to start this talk by talking about the basics of channels so talk about how to create them how we can use them how we can pass data through them then we'll talk about how we can restrict data flow now a basic channel is a two-way street we can send data in and we can get data out but that's not always what you want to be able to do with the channel sometimes you want to send only Channel or a receive only Channel and we'll talk about how to do that in the second section then we'll talk about buffered channels and how we can actually design channels to have an internal data store so that they can store several messages at once just in case the sender and the receiver aren't processing data at the rate then we'll talk about how we can close channels once we're done with them we'll then revisit the topic of for range loops and we'll learn how we can use channels with a four range loop and then we'll wrap up our discussion by talking about select statements which is kind of like a switch statement but specifically designed to work in the context of a channel okay so let's go ahead and dive in and learn the basics of working with channels so when we're working with channels in the go language we're almost always going to be working with them in the context of go routines and the reason is because channels are really designed to synchronize data transmission between multiple go routines so let's go ahead and get started by creating some go routines well actually the first thing that I need to do is I need to create a weight group because as you'll remember from the last video we use weight groups in order to synchronize go routines together so we're going to use the weight group to synchronize the go routines to make sure that our main routine waits for all of our other go routines to finish and then we're going to use channels in order to synchronize the data flow between them so we've got two different synchronization mechanisms going on in this little application the next thing we need to do is we need to create a channel now channels are created with the built in make function and there really is no shortcut around this now a lot of uses of the make function you can actually use other forms when you're creating a channel there's enough internal mechanisms that need to fire that you have to use the make function in order to allow the runtime to properly set up the channel for you now in the simplest form of the make function with working with channels we're going to use the Chan keyword to say that we want to create a channel and then we're going to provide the data type that's going to flow through the channel now you can pick any data type that you want we're just going to be using integers here but keep in mind that this means that the channel is strongly typed you can only send integers through this channel that we're creating here similarly if we provided strings we can only pass in strings if you provided pointers to integers you can only send in pointers to integer you get the general idea so when you create a channel you're going to create that channel to accept messages of a certain type and it's only ever going to receive them send messages of that type now in this initial example we're going to have to go routines I'm going to spawn so I'm going to add two items to my weight group and then we'll go ahead and create those go routines and then we'll talk about those so let me just drop the rest of the code here and you can see my first go routine is an anonymous function actually both of them are and this first one is going to be receiving data from the channel so this go routine is actually going to be my receiving go routine and then this channel is actually going to be my sending go routine so the way that we send a message into a channel is as you see here we're going to use this arrow syntax so we're going to use a left fan and a dash and when we're putting data into the channel we list the channel first then we have this arrow and then the data that we want to pass in so imagine that the arrow is pointing in the direction that we want to data to flow so we want the data to flow into the channel and so the arrow is pointing toward the channel similarly if we want to receive data from the channel then we're going to put the arrow on the other side so we're going to use that same less than and - but it's going to be before the channel and so we're going to be pulling data from the channel so in this line right here on line 14 we're going to be receiving data from the channel and assigning it to the variable I and then after we're done we're going to call the done method on our weight groups and we're just going to print the value out so all we're doing here is this go routine is going to be sending the value 42 this go routine is going to be receiving whatever value comes out of the channel which in this case of course will be 42 and it's going to print that out to the console so let's go ahead and run that and we see that in fact it does work so the nice thing about doing this is since we're sending a copy of the data to the channel we could manipulate the variable assigned here so for example we could actually start this off with I set equal to 42 and we can pass in I and then afterwards we could reassign I and it doesn't matter because like with all other variable operations and go when we're passing data into the channel we're actually going to pass a copy of the data so when we manipulate it afterwards the receiving go routine doesn't really care that we change the value of the variable it's not affected by that at all now another common use case for go routines is if you have data that's asynchronously processed so maybe you can generate the data very very quickly but it takes time to process it or maybe it takes a long time to generate that data so you've got multiple generators but it can be processed very quickly so you might want to have a different number of go routines that are sending data into a channel then you have receiving so let's take a look at how we can do that so it's a slight modification to the example that we just went through instead of just having the go routines fire once I'm actually creating go routines inside of this loop here so I'm going to create five sets of go routines so each one of the groups is going to have a sender like we have here which is exactly what we had before and then we're going to have a receiver which is again just like we had before so by the time the application is done we're going to spawn ten go routines here five senders and five receivers and all of them are going to be using this single channel to communicate their messages across so if we go ahead and run this we see that we do get five messages received okay so this works that really well but I will warn you if you start playing around with this and you decide to start moving the senders and receivers to make the main symmetrical things won't work very well so one of the things you might want to do to play with this example is take disco routine and move it outside of the for loop so you're going to have one receiver and multiple centers at the end of this well that actually isn't going to work right now because if you think about how this go routine is going to process it's going to receive the message coming in from the channel it's going to print and then it's going to exit but then down here in the loop we're actually going to spawn five messages into that channel so we can only receive one but we're sending five and if we run this we're actually going to run into a problem and that is we see all routines are asleep that we have a deadlock condition and the reason for that is because we have these go routines down here that are trying to push messages into the channel but there's nothing that can process them now an important thing to keep in mind here is the reason that this is a deadlock and the reason for that is this line of code here is actually going to pause the execution of this go routine right at this line until there's a space available in the channel so by default we're working with unbuffered channels which means only one message can be in the channel at one time so our first go routine in this loop gets happily spun up it pushes a message into the channel and then it exits and it causes this done method on the weight group and then that message gets processed by this go routine here and everything's happy however this go routine then exits and then our next go routine comes along and tries to push another message in well it blocks right on this statement and there's nothing in our application that's going to receive that message and that's why we see the go runtime notice that and it's going to kill the application because it notices that we have a problem and it doesn't know how to resolve it now I want to go back to our previous example and actually I'm going to modify things slightly here because I want to show you that notice that we're just working with the raw Channel so this is perfectly valid code for us to write as a matter of fact if I go ahead and run this we see that we get two messages printed out but look at how that's happening so this go routine is pushing a message into the channel that message is then being received up in this go routine and print it out this go routine then the one that received this message is then putting a message back into the channel and that is then being received down here in this go routine which is then printing the message out so both of these go routines are acting as readers and writers now that may be a situation that you want but very often you want to actually dedicate a go routine to either reading from a channel or writing to a channel so in order to do that what we're going to do is we're actually going to pass in the channel with a bias on the direction that it's going to be able to work with so the way we're going to do that is by accepting variables in our go routines so we'll start with this first one here and we want this to be a receive only channel so the way we're going to do that is we want data to flow out of the channel so you notice we're using that similar syntax we're going to list the type of the channel and then we're going to have this arrow coming out of it so data is flowing out of the channel and so this is going to be a receive only channel similarly if we want a send only channel we're going to give it the variable name we're going to say that it's a channel now we put the send only operator right here and then we put the data type so this is going to be sending data into the channel only and this is going to be receiving data from the channel and then of course we have to pass the channel into the go routines as arguments so when we run this we're actually going to get an error and the reason we get an error is because we're trying to pass data into this channel but this is a receive only channel so it's invalid to send data into it and then similarly we have an error down here on line 21 because we're trying to receive data from a send only Channel so if we go ahead and wipe out these lines here this line and this line and run then everything works as it did before but now it's much more clear what the data flow is in the go routine we know that we're going to be receiving data on one side and we're going to be sending data on the other now something that's a little unusual with this is notice that we're passing in a bi-directional channel so this is just a normal channel and we're receiving it a little bit differently so this kind of feels like a little bit of polymorphic behavior and this is a special aspect of channels the one time understands this syntax and so it actually is going to I'm going to use the word cast here it's going to cast this bi-directional channel into a unidirectional channel but that's not something you can generally do in the go language that is something that is specific to channels now one of the problems we ran into on a previous example is we had a situation where we tried to push five messages into a channel but we only had one receiver and we noticed that the application deadlocked well we can get around that in a couple of different ways now I'm going to show you one way to get around that that really isn't ideal for solving that problem but I will talk about the problem that it is solving and that is by using buffered channels so I go ahead and paste in this example here we will see an example of the problem we might run into I've simplified it a little bit from the previous example we ran into so we've got our initial example where we've got a receive only go routine we've got a send only go routine but in our send go routine we're actually sending two messages but since we're only receiving one we expect that we're going to run into a problem so let's go ahead and run this and we see that we do in fact have a problem we receive the 42 out and printed it but there's nothing to deal with this message here that's in the channel and so the application blows up because this go routine can never complete because it's blocked on this line so we need a way to get around that now a simple way to get around that is by simply adding a buffer here so if we add a second parameter to the make function up here and provide an integer that's actually going to tell go to create a channel that's got an internal data store that can store in this case 50 integers now what that's going to do is it's actually going to allow our application to complete but we do have a little bit of a problem here because this message is lost so it did eliminate the panic and I guess in one way you could say it solved the problem but it did create another problem in that we lost this message now this isn't the problem that buffered channels are really intended to solve but I do want to show you that it does create that internal store so we can receive multiple messages back out as a matter of fact what we can do is we can just copy this line down here and reformat this and we don't need this colon right here and if we run this we see that we do get both messages printed back out now what a buffered channel is really designed to do is if the sender or the receiver operate at a different frequency than the other side so you can imagine if we had a data acquisition system and maybe we retrieve data from our sensors and a burst transmission so maybe we're acquiring data from seismometers and we're monitoring earthquakes well maybe those seismometers in order to conserve power don't send their data continuously they're going to send a burst transmission maybe once an hour so every hour we're going to get a burst transmission that maybe last five or six seconds that's going to contain the entire hours worth of data so in that case our sender is going to be inundated with data when that bursts happens and it's going to have to have a way to deal with it well the receivers might take a little while to process that data so in that case what we might want to do is create a buffer here of these signals that are coming in from our seismometer that's going to be able to accept that one hours worth of data and then our receivers can pull that data off as they're able to process it and keep things working smoothly so that the channel that's receiving the data from our sensors doesn't get locked up because it doesn't have a place to put the next message so that's really what buffered go routines are designed to work with is when your sender or your receiver needs a little bit more time to process and so you don't want to block the other side because you have a little bit of a delay there now this isn't the right way to handle this situation what is the right way well the way that we typically handle something that's going to happen multiple times such as passing a message into a channel is by using some kind of a looping construct and that's no different with channels as is with anything else so let's paste in this example where instead of processing the message once and then having this first go routine exit I'm actually going to use a four range loop but notice what I'm ranging over instead of ranging over some kind of a collection such as an array a slice or a map I'm actually ranging over the channel now the syntax changes just a little bit because if this were a slice the first index that we pull back is going to be the index in the slice and then the second variable we pulled out if we have for example a second variable here would be the value well when you're ranging over a channel things are a little bit different when you pull a single value you're actually going to get the value that's coming out of the channel and so if we run this we see that we do in fact get 42 and 27 but we still have a deadlock condition so what's causing that deadlock condition well before we had this four range loop we actually deadlock this go routine right here and everything died well in our new application were actually dead locking in the four range loop and the reason for that is because we're continuing to monitor for additional messages but we stopped sending messages and so now this four range loop doesn't know how to exit and so this go routine is now causing the deadlock condition so we've improved the situation we kind of move the needle where we're no longer dead locking in our sender but we are still dead locking in our receiver so how do we handle that well the way that we're going to handle that is we have to understand how the four range loop works so if you're using a four range loop over a slice how many times does that iterate well it executes the loop once for every item in the slice so if you've got a slice with five elements in it you're going to run through the four range loop five times well how many elements are in a channel well there could be an infinite number of elements in a channel because you can constantly push a new message into it so what is the way to signal a four range loop with a channel that there are no more messages coming well the answer is we need to close the channel so anything that has access to the channel can do this we're going to use the built in closed function and we're going to pass in the channel like you see here so what we're doing on our sending side is we're passing in two messages we're passing in 42 and 27 and then we're letting the channel know we're done working with you so we're going to go ahead and close the channel this full range loop is going to detect that and when we run this now everything runs well because we're passing in the message 42 that gets processed in the for loop or passing in 27 that gets processed then we close the channel that gets processed by the for range loop which notices that the channel is closed and it's going to exit and it's going to terminate the loop so when we terminate the loop then we call the done method on the wait group and then we exit the go routine all of our go routines exit properly and we have no more dead locks now we do have to be a little bit careful in closing channels because when you close a channel you really have to mean that you're closing the channel down so let's try closing the channel right here and then pushing another message into it so if we run this we actually get a bad thing happening so in this case the application panicked and why did it panic because we tried to send a message on a closed Channel so the issue here is we closed the channel right here on line 21 and then on line 22 we tried to pass another message into it so that is a no-no you are not allowed to pass a message into a closed channel because the channels closed so you might ask well how do I recover from this how do I reopen the channel or undo that or whatever and the answer is you can't as a matter of fact you can't even detect if a channel is closed except for by looking for the application panicking so call that a limitation of the NGO language or not I don't know but you do have to be very careful that when you close a channel nothing else is going to send a message into it so if that is a possibility then the only option you really have is to have a deferred function and use a recover in there to recover from the panic that gets thrown because in this situation you will have a panic and there is no way to avoid it so it's in your application that's a situation that's likely to happen then again you're going to have to use that recover function and you can review the video where I talk about using those now on the receiving side we do have a little bit of a different story here because this issue is on the closing side so we cannot send a message into a closed Channel and we can't detect if a panel is closed before we try and send a message into it however if we go on the receiving side then the story gets a little bit brighter so you might ask the question how does the for range loop know that the channel is closed it has to have some way of detecting it what turns out that there's more than one parameter that you can pull back from the channel so just like when we're querying maps and we're trying to get a value out of a map and we can use that comma okay syntax well that syntax works for channels as well so if I this example up a little bit and this is going to do exactly the same thing is our current example here using a full range loop but instead of using the four range loop and having go automatically process the closed channel for us we're going to process this manually so let me paste in this example and show you so notice that I'm in a for loop in this go routine and I don't have any conditions on it so this is going to execute forever down here then I'm receiving a message from the channel and I'm using the comma okay syntax so I'm going to get the value from the channel in I and I'm going to get a boolean letting me know if the channels open or not in the okay variable so if the channels open then okay is going to be true if the channel is closed then okay is going to be false so the happy path if okay is true then I'm going to go ahead and print out my message otherwise I'm going to break out of this for loop here because the channel is closed and I'm not going to be receiving any more messages from it so this is functionally exactly the same as the for range construct but we're explicitly seeing this comma okay syntax so which one would you use well in this situation it would make more sense to use the full range construct but there may be situations where you're receiving data from a channel and you are not in a loop so maybe you're spinning off a new go routine for every time you're processing a message and so the loop is going to contain the spinning off of the go routines and so you're going to need this comma okay syntax because it might not make sense to use the for range loop now the last thing that I want to talk about in this video are what are called select statements so let me go ahead and paste in this code here so we talked about in the last video how there can be situations where you create go routines that don't have an obvious way to close and that's what I want to try and illustrate here so if I go ahead and run this we see that we do get these messages printed out so I'm just doing a simple logger implementation so as you see here is I've got some constants that are declaring my log level I've got a struct that I've declared that's holding the timestamp for the log entry the severity of the log level and then whatever message I'm trying to print out then I'm creating a log Channel and the way this application works is the first thing the main function does is it spins up this go routine that's going to be my logger and what it's going to do is it's simply going to monitor that log Channel for log entries that are coming from throughout my application so the ideas i've got a central logger and anything that could do logging in my application just needs to know about this channel and all of my logging logic can be hidden within the processing of those log channel messages so the logger is down here we've got a full range loop that's listening for messages from the log channel and all it's doing is it's printing out a formatted message that's got the timestamp it's got the log level and it's got the message from the log so no big deal here nothing terribly exciting then my main function goes on to exercise that a little bit it sends two messages into the log Channel one letting it know that the application is starting another one letting the application know it's shutting down and then I've got a sleep call here just to make sure that the logger co-routine has enough time to process that now you notice my timestamps are a little funny here that's because I'm working with the playground I promise you this code does work if you shift it over to Visual Studio code you will actually get real-time stamps but for some reason the playground doesn't give you the current time when you call the now function and so this is just something that we're going to have to work with in this example now the problem I want you to consider is when does the logger go routine close down so obviously the logger go routine has to terminate sometime because the program finishes execution and we get the results back from the playground so what's happening here is remember an application is shut down as soon as the last statement of the main function finishes execution so when we finish this sleep call here the application terminates and everything is torn down and all resources are reclaimed as the go runtime returns all of the resources that it was using back to the operating system so what that means is that our logger go routine is being torn down forcibly there's no graceful shutdown for this go routine it's just being ripped out because the main function is done now in some situations like this one that may be acceptable but there are many situations where you want to have much more control over a go routine because remember what I said in the go routine video you should always have a strategy for how your go routine is going to shut down when you create your go routine otherwise it can be a subtle resource leak and eventually it could liek enough resources that it could bring your application down so there's a couple of different things we could do here right we could of course do a defer call here we can pass in an anonymous function and inside of that we could go ahead and close the log Channel so what that's going to do is when the main function exits it's going to go ahead and close the channel and then we are gracefully shutting down that channel and that works just fine there's no issues with that we are intentionally closing down the channel we know how our go routine is going to close and so this is perfectly acceptable in this youth case but this isn't what I want to show you so this is certainly something you could use in this use case but I want to show you another way that very commonly used in these kind of situations so the way that I want to show you is using what's called a select statement so let me go ahead and paste in that code and we'll walk through that so the application is basically the same I've got the same Constance of here I've got the same struct I do have this additional channel here and notice the type signature for it so it's strongly typed but it's strongly typed to a struct with no fields now struct with no fields in the go language is unique in that it requires zero memory allocations so a lot of times you will see a channel set up like this and the intention is it can't send any data through except for the fact that a message was sent or received so this is what's called a signal only channel there's zero memory allocations required in sending the message but we do have the ability to just let the receiving side know that a message was set so this is pretty common you might be tempted if you're new to the language like I first did is you send a boolean in here but that does actually require a variable to be allocated and copied so it is actually better to use an empty struck because it saves a couple of memory allocations it's a little bit minor but it is something that if you are going to use the channel as a pure message then you might as well go with the conventions and use this approach so our main function is exactly the same as it was before we've got our lager we've got our log channel sending in a couple of messages and then we got a sleep call here and then inside of our lager function we've got an infinite loop now and we're using this select block so what the Select statement does is the entire statement is going to block a message is received on one of the channels that it's listening for so in this case we've got a case listening for messages from the log Channel and the case listening for messages from the done channel so if we get a message from the log Channel then we're going to print out our log entry if we get a message from the done channel then we're going to go ahead and break out of this for loop so what this allows us to do is at the end of our application we can go ahead and pass in a message into our done Channel and that is going to be an empty strike and I'm just going to define that empty struck on the fly here so this is a little bit confusing syntax but this is the type signature for my struck so I'm defining a struct with no fields and then I'm initializing that struck using these curly braces here so if I go ahead and run this you see that the application runs properly so I do process my log messages and then I pass in this message into my done channel when I wish the logger to shut down so this is a common situation for you to use when you're monitoring channels and you need a way to have the go routine that's monitoring those channels be able to terminate so very often you're going to send in normally as a parameter you're going to send it in this done channel and then whatever is ready to kill the go routine will go ahead and send a message into that done channel and it'll go ahead and kill it now one more thing that I do want to talk about and I'm not going to actually run it because it's going to break our application here but you can't have a default case here and if you do then this no longer becomes a blocking select statement so what this is going to do is if there's a message ready on one of the channels that are being monitored then it's going to execute that code path if not it will execute the default block so this is useful if you want to have a non-blocking select statement then you need to have the default case in there if you don't have the default case then the Select statement will block forever until a message does come in okay so that's what I have to talk about with channels let's go into a summary and review what we've talked about in this video we talked about channels and how we can use them to synchronize data transmission between go routines we started out by talking about the basics of working with channels and we learned that we can make our channels using the built in make function and how that's really the only way that we have available in the go to make a channel when we do make those channels those channels are strongly typed so we're going to use the Chan keyword to indicate that we wish to create a channel and then we have to follow that with the data type that the channel is going to be able to send and receive now that data type can be anything it can be a primitive like we see here with an integer it can be a struct it can be an interface but it does have to be strongly typed we can send a message into the channel using this arrow syntax and the position of the arrow kind of indicates the direction that the data is going to flow so in this case we list channel we have the arrow and then the value that we wish to send into the channel so notice that the arrow is pointing into the channel but when we want to receive messages from the channel then the arrow is leading out of the channel and so we're going to use the same arrow syntax but the channel is going to be added after the arrow instead of before and we can't add multiple senders and receivers as a matter of fact it's very common as a matter of fact is very common for one channel to be distributed among multiple go routines and that way you can have multiple data generators that are sending messages into the channel as well as multiple data receivers and that allows you to balance the performance between senders and receivers so if you can generate data ten times as fast as you can process it then you can create 10 times as many receivers and that way you can balance the workload out between senders and receivers we then talked about how to restrict data flow bind default channels are bi-directional constructs so you can send and receive data into a channel now very often what we want though is our go routines to be designed to handle channel data only in one direction so we saw that we can do that by passing in the channel but then on the receiving side so for example in the argument list of the function we can actually specify the direction that we can work with by again adding that arrow and we either add it before or after the Chan keyword depending on what kind of channel that we want to make we can make a send only Channel by putting the arrow after the Chan keyword and we can make a receive only channel by adding the arrow before it we then talked about buffered channels and how buffered channels contain internal data stores that allow us to get around this limitation of channels that by default a channel will block the sender side until a receiver is available and the receiver side will be blocked until a message is available to come out of the channel so you can actually block a go routine on the sending side or the receiving side of the channel so if there's no position available in the channel to add the message then the sending side will be blocked until the space does become available and if there's no message in the channel then the receiving side is going to be blocked until a message becomes available for it to work with so in order to decouple that we can add an integer as the second argument to the make function and that's going to allow the channel to have an internal buffer to be couple your senders and receivers just in case there are situations where data is generated faster than it's received so just like it says here we want to use buffered channels when sending and receiving have a symmetric loading so if we can generate messages faster than we can receive them then a lot of times a buffered channel is a really good way to go we then moved on to talk about for range loops and specifically how to work with them with channels and we learn that they basically work the same way but there are a couple of subtle differences the first thing is the first parameter that you're going to receive from the for range loop when working with channels is the value itself not the index like we saw when we were using for range loops over arrays slices and maps and we saw we could use for range loops to monitor channel and process messages as they arrived so the for range loop is just going to keep pulling messages as they come in off the channel and it'll process them as they come then when the channel gets closed the for range loop is going to detect that and it will go ahead and exit the loop and finally we talked about select statements and how they work kind of like switch statements but they work only in the context of panels and how they allow a go routine to monitor several channels at the same time now if they block if all channels are blocked so if there's no messages available on na channel then the Select statement will block by default and then when a message comes in it will go ahead and process that on the proper case if multiple channels receive a value simultaneously then the behavior is actually undefined so because of the highly parallel nature of many go applications you can get into situations where messages arrive on two channels at virtually the same time so one of those cases will get the nod from the select block but you can't be sure of which one's going to get it so there is no rule like in switch blocks where the first one matches is going to get it it could be anyone so the ordering of the cases in your select statements really doesn't matter from the standpoint of how those conflicts are going to get resolved now if you do want a non-blocking select statement remember that you can add that default case in there so if there are no messages on any of the monitored channels then the default case will go ahead and fire and so the select statement will process and execution of the go routine will continue from there okay so that wraps up what I have to talk about with channels and really it brings us to the end of the discussion that I have for this introduction to go series I want to be honest with you that hearing your feedback and seeing the value that you've received from these videos over the last couple of months that I've been putting them together has really been gratifying and it's made this project a lot of fun so if you have any questions or comments keep them coming I love to see the feedback and I love the seeds of recommendations for videos that you would like to see in the future for now this is Mike VanSickle wishing you luck in all of your gopher endeavors take care
Info
Channel: Failing Forward
Views: 17,617
Rating: undefined out of 5
Keywords: golang
Id: SgifAwaxMQU
Channel Id: undefined
Length: 35min 14sec (2114 seconds)
Published: Sat May 13 2017
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.