So I wanted to start out with an introduction to the go language itself. Now I know that this is much storied territory, but I have to start somewhere. And this is going to serve as a stepping off point for us to go through a survey of the entire go language, as well as hitting some of the key libraries along the way. So the first thing that we need to know is that go is created by a small team within Google. And that team was made up of Robert griesemer, Rob Pike, and Ken Thompson. Now these
guys have been around the software industry for a little while, for example, Ken designed and implemented the first Unix operating system, as well as had a key role in the development of Unicode. So when these guys got together and decided that they wanted to create a language, we had a lot of talent in the room as soon as these guys got together. But one of the questions that we need to understand is, why create a new language at all? Well, to understand that, we have to look at the Languages that are common inside of Google.
And that's the time that go is being designed, there were really three languages that were key. The first is Python, then Java, and then C and c++. Now, each of these languages in and of itself is very powerful. However, the go designers started to recognize that there were some limitations that Google was running into, that might not be able to be fixed, given the history and the designs of the existing languages. So for example, when we look at Python, Python is very easy to use, but it's an interpreted language. And so it can be a
little bit difficult to run applications at Google scale that are based on Python, it can certainly happen. But that is one of the challenges that you can run into in very large Python implementations. Java is very quick, but its type system has become increasingly complex over time. Now, this is a natural trend that a lot of languages go through. They start out very simple, but it has additional use cases and additional features are layered into the language, it becomes increasingly More difficult to navigate things C and c++ is quick as well. However, it suffers from
a complex type system. And additionally, its compile times are notoriously slow. Now, the type system has been receiving a lot of attention lately in the C and c++ communities. However, they still have the burden of needing to manage all that legacy code. And so similar to Java, it's very difficult for them to move past the history that they have, because c++ applications written 10 years ago still need to compile today. And the slow compile times are another legacy issue that C and c++ have inherited as well. When C and c++ were designed computers had nowhere
near as much memory as they do today. So the decision was made to optimize the compilers to use a minimum amount of memory. And one of the compromises that that brought about was the compile times can be a little bit sluggish. In addition, all three of these languages were created in a time where multi threaded applications were extremely rare. Almost every application that was created really had to focus on a single thread at A time. So concurrency patterns built into these languages are patched in at best. And so working in highly parallel, highly concurrent applications
like Google often runs into can be a little bit challenging when working in these three languages. So enter go, What does go bring to the party in order to address some of these concerns? Well, the first thing that we need to understand is gozi, strong and statically typed language, so it inherits that same feature set from Java and c++. So what do we mean by strong and statically typed with strong typing means that the type of a variable cannot change over time. So when you declare a variable a to hold an integer, it's always going
to hold an integer, you can't put a Boolean in it, you can't put a string in it. And static typing means that all of those variables have to be defined at compile time. Now, are there ways around that? Yes, go does have features that allow you to get around its type system, but 99% of the time, you're going to be living in a world that's strong and statically typed. And getting all the benefits that come with that. Now, if you come from languages such as Java, then you might be a little concerned that strong and
statically typed languages tend to be a little bit verbose. Well, we'll see as we get into some go syntax, how there's been a lot of effort taken to make the compiler do as much work to understand what you're talking about with a variable. So you don't have to explain to the compiler every time what your variable types and things like that are. In addition, go like a lot of recent languages has a strong focus on the community that supporting it. Just because go is an excellent language does not guarantee success. Because there are so many
languages out there, that it can become difficult for a new developer to ramp up in any one. As a result, there's a strong community built up around go that's really focused on making sure that the go language keeps moving forward, and that new developers have an easier time as possible ramping up onto the language. So what are some of the Key features of the language itself? One of the first and I would argue most important features that go has is a recognition that simplicity is a feature. So as we go through and start learning about
the go language, you're going to run into some features and you're going to ask yourself, Well, why doesn't this exist? Or why don't we have that feature? And a lot of the reasons Come back to this feature. There's a recognition that if the go language recognizes simplicity is important, then that means that we're going to have to decide to leave out some other features that might be very useful, but would add complexity to the language. Additionally, go focuses on extremely fast compile times a lot of modern development environments to write code fast to build it
fast and to test it fast and get feedback back to the developer as quick as possible. Well, if you've got a 45 minute compile time, it breaks that cycle. And developers have a very hard time staying in that design build test loop. And so go focuses on keeping those compile times down, even though it's Going to yield us fully compiled binaries at the end, go as a garbage collected language, which means that you're not going to have to manage your own memory. Now you can manage your own memory. But by and large, the go runtime
is going to manage that for you. And the reason for that gets back to this simplicity argument, there is a recognition that a garbage collected language does have challenges when dealing with certain use cases. For example, real time trading systems for stock market systems have a very hard time when you're dealing with garbage collection. However, the advantages on the developer of not having to manage their own memory all the time, were deemed to be more important. Now, that doesn't mean that the delays that a garbage collector incurs haven't been paid attention to, if you go
back through the history of the go language, you'll actually see the past few versions have had a huge emphasis on reducing the amount of time that the application has to pause during a garbage collection cycle. And at this point, they're actually really Fast, almost to the point that you don't know that a garbage collection happened, in order to address that concern of concurrency go does have concurrency primitives built right into the language. And we'll talk about that as we go through some of these videos. But instead of having a library that we're going to have
to import, in order to work with concurrency, we're going to be able to do concurrent development right there in the base language. Finally, go compiles down to a standalone library, which means when you compile your go application, everything is going to be bundled into that single binary that's related to the go application itself. So the go runtime is going to be bundled in there, any libraries that you're depending on, they're going to be compiled in there. So you don't have to worry about reaching out to external libraries and DLLs, and things like that in order
to make your application work. And what that gives you is version management at runtime becomes trivial. Because you simply have one binary, you deploy that binary, you run it, and all of its dependencies are there. Now keep in mind, when I say dependencies, I mean to go dependencies, if you're going to build a web application that has HTML, resources, and CSS, those have to be bundled along with the binary, but the binary itself is standalone and self contained. Okay, the next thing that I'd like to do is show you some of the resources that are
available to you, as you start to explore the go language. One of the most useful resources that you're going to be able to take advantage of as you're ramping up on go, is goes website
[email protected]. Now, why is it go lang.org? Well, if you take a minute to think about a language called go, that doesn't really lend itself to unique search results in Google or Bing. So go lang.org it is, as a matter of fact, a lot of places that you see go mentioned, you're gonna see it actually described as go Lang, because that makes
it a little bit more unique when you're looking for search results. So the first thing that you might notice, as we go into the homepage here is this isn't really Laid out like a lot of traditional home pages. This, in my opinion, is very much an engineering homepage. So instead of a lot of design aesthetic, this gets right into the engineering aspects and shows you how to start working with the language. So this yellow box over here is going to be your entry point for your first go application. So if we go ahead and click
this run button, you see that we almost instantly get an application sent back to the server, it gets compiled, and it gets run for us. So we can start playing around with go code without installing anything on our local machines. And we're actually going to take advantage of that through this first few videos. As a matter of fact, if I make a small change here, so maybe if I will say, Hello, YouTube peoples and run that again, then I'm saying hello to y'all, so Hi. So it's as simple as that in order to get started
with the go program. Beside that window, we see this download GO button. And that's going to take you to resources that you're going to be able to use in order to download the latest go binaries, as well As download older versions of the runtime. And if there's an unpublished version, for example, at the time I'm recording this go 1.8 is at RC two, you can go ahead and download that install that and check that for bugs and play around with new features in the language. If we come across at the top, we see this documents
link here. And this is going to be another critical resource as you're starting out with the language. As a matter of fact, you're going to refer back to this page quite often. But if you really want to walk through on the website, a tour of the go languages, and I would recommend you go to this Getting Started link. This is going to get you started downloading and installing the go compilers and tools and things like that. And you can see as we navigate there, it's going to show you the different architectures that you're going to
be able to use with go and there's quite a few and how to get started on each one of those. If we keep continuing down the tour of go is kind of an introduction to the go language that takes you through a gradual Introduction. So it's going to start out with some very simple Apple And then build up more and more and more and help you understand what's going on with go concurrency and things like that. Effective go is a very useful article, especially as you start to mature in your understanding of the language, and
really understand how the go language is used. So I would encourage you to go into that it's a pretty lengthy read. But you should consider this required reading if you're actually going to start building non trivial go applications. But we're not going to worry about that right now. We got plenty ways to go before we need to get through all of this stuff. And then down here at the bottom is some reference information. This is more advanced documentation that you're probably not going to need right away. But for example, the command documentation gives you a
lot of information about the go tool itself that you're going to use for local development with go, there's a lot of things that the go program does. And this is going To help you understand how to navigate that. The packages link is perhaps where I spend the most time on gos website. And this gives you documentation for all of the libraries that are built into go. So when you install go and you install the go binaries and tools, you're going to get all of these libraries available to you. So just scanning down, you can see
that we've got different libraries that are targeted at working with archives, we've got some cryptography libraries, database drivers, continuing to go down, we've got some things for working with HTML, and network traffic. Now, some things that you might find missing here are we don't have any GUI libraries. That's because at this point go really isn't focused on the use case of client application development. So go is really targeted at building servers and web applications. And so that's where a lot of the libraries are going to be focused on. There are some projects that are working
on mobile applications using go as well as client side applications using go but they're not officially supported at This point. If we come over to the project link, we're going to find some information about history of the project, what releases have come out and when, as well as some links to mailing lists and resources that you can take advantage of. If you want to keep track of the development of go as a language as well as if you find an issue in the go language, you can see some information here on how to report that
issue. And then we've got the Help link here. And this is going to be one of your more important links as you get started here because this is going to be your on ramp into the community. Now the two most active in my experience are the go forum, which is a nice discussion forum that allows you to post your questions and get people to answer back. But if you want something a little bit more real time then the gopher slack is a Slack channel specifically targeted at NGO development. And there's multiple sub channels in there
for new developers for library developers. Even a lot of the NGO meetup groups have their own sub channels on the gopher slack. So if You want to get on the gopher slack that I would encourage you to come over here to another website called go Lang bridge. And this is what I consider the on ramp to the NGO community. Because NGO Lang bridge is specifically there to advocate for the go language, and to make sure that the community is healthy and strong. As I said, one of the key aspects of the go language is a
focus on having an excellent community. And really, it's go Lang bridge and the awesome people that support it, that are making that happen. So if you scroll down a little bit, you can see some links to the online communities. If you want to join the Slack channel, you do have to receive an invite. So this link here is going to take you to the forum that's going to allow you to get that invitation. And there's no problem getting the invitation, the only thing that they asked you is to read the community guidelines, there is a
code of conduct that just make sure that everybody is going to be treated respectfully in the community, just to make sure that we're all here trying to help each other out. And The last thing that I want to show you on the website is this play link here. Now this link just flies out an editor. And this is really nice, because this is available throughout the site. So if I come to the packages, and let's just say I dive into the network package, and I'm learning about some network function, then I can go ahead and
pop into the play, I can create a real quick proof of concept go application in order to make sure that I understand how that's working. And again, just like we saw on that homepage, if I click run that I can go ahead and execute that. Now there are some limitations, obviously, this application is sent to the back end. And there are some limitations, you're not gonna be able to read the file system at the back end, for example. But a lot of the things that you want to play around with, you can play around with
in this online environment. Now another place to get at this playground is over here at play that golang.org. And this is the last thing I want to show you in this introductory video. So This is going to be the environment that we're going to focus on. And actually, let me make that a little bit bigger. So maybe it's a little easier for you to see. But this is going to be the environment that we're going to focus on as we start to learn the go language. So we're going to learn the basics of go application
here, we're going to start playing around with how we're going to work with variables and logic and looping and things like that. Now, eventually, we'll get to installing a local environment. And you can certainly take advantage of the other resources on go Lang website, if you want to get there before I create a video on it. But I think that there's a lot that we can talk about without making a commitment to setting up a local development environment by just going through this playground here. So if we take a second look at this application, we
see some of the key aspects of any go program. And the first thing that you see at the top is this statement package main. Every go application is structured into packages. So every go file that you're going To have is going to have to declare what package it's a part of nain is a special package, because main is going to be the entry point of any one of your applications. Down below that we have an import statement. And this is the statement that we're going to use in order to import additional libraries. So this library
is called thumped, which Yeah, you actually say that in the NGO community, I can't bring myself to say that. So if I call that FMT, I hope that you'll forgive me. But in the NGO community, you will often hear this called thumped. And this is the package is going to allow us to format strings. So you see down below here and our main function, which is the entry point of our application, so the main function in the main package is always going to be our application entry point. And this is going to be where we're
going to contain our first code that's going to run in go. So we're going to call into the FMT library. And we're going to pull out its print ln function, and that print ln function takes one argument, and that argument in this Case is a string. So we're going to print out Hello playground. Now if I go ahead and run this, then down below at the bottom of the screen, here, you see Hello, playground gets printed out. And then it says program exited. If we have an error in the application, say if I delete this
quotation mark and run, then you're going to get a compiler error printed out at the bottom, that's going to help you debug your application. So this online environments going to be very good for you to get started, because it's going to help you through understanding what's going on. So for example, we see here in line eight, it got an unexpected semi colon or new line, when instead it was expecting a comma or a parenthesis. And the reason for that is because this closing parenthesis actually became part of the string. So the line terminated early, and
it didn't have an end to the function call. So if we go ahead and re add the quotation mark, and run, we're good to go. And we've got our first start in a go application. So I hope that this was helpful for you a little Bit of background in a language that you're going to be learning I always find is a little bit valuable, it helps to understand the motivations for the creation of the language and the major features, in order to understand what problems that language is going to try and solve and how it's
going to go about trying to solve them. What I want to show you is how to get started setting up your own local development environment to program with go. Now I know in the last video, I showed you this website here, and I said that this is what I want to use. That is the website it
[email protected]. And I said that this is the website that I want to use in order to show you a lot of concepts that you're going to need to be familiar with with the go language. And that's still my plan.
But as I thought about it, I decided that it doesn't really make sense to force you to use the playground, just because it's a really good place for me to demonstrate concepts. So I want you to have all the tools available to you to set up your own local NGO development environment. So you can play Around with creating your own applications as you learn about this wonderful language. So the first thing that we're going to need to do is we're going to need to download and install the go language in the go tools itself. And
so in order to get started with that, we're going to start over
[email protected]. And we're going to click on this download go link here. Now this link is going to take you to all sorts of different versions of go. But in general, if you're getting started, just pick the latest stable version that's available for you, and it's not going to steer you wrong. Now, if you're a Windows user, then I would encourage you to click this MSI link, it's going to download an installer and go is going to be put on your system automatically. However,
if you're using os 10, or Linux, then I would recommend that you go to this installation instructions link here and follow the instructions here. Now, you're still going to need to download the go binaries. But if you scroll down just A little bit, you're going to see this command here. And this is going to give you the tar command that you're going to need to use in order to unpack the go binary and install it onto your system. So I've already done that. And I can show you that by opening up a terminal here. And
the default location to put go is in the user directory underneath local and then in a folder called go. So if we look at there, and they look at the contents there, I see that I have all the go tools installed. So with the Windows Installer, all it's going to do is it's going to place these into C colon backslash go. And I would strongly encourage you if you can accept these default locations that you go ahead and do that because it's going to make setting up your environment just a little bit simpler. Now after
we get go installed, we have a little bit of configuration in our environment in order to be able to use go effectively. So I'm going to come back to my home directory here. And I'm going to make a change to my bash RC file. Now I'm on Ubuntu. So it's going To be in my bash RC file. If you're an LS 10, it's going to be your bash profile. If you're in Windows, basically what we're doing is we're setting environment variables. So if I open that up and come down to the bottom, then I see
that I've got the pre generated bash script. I'm not going to really worry about that. But there's a couple of variables that we're going to need to set. Hello, everybody, I need to pause the video here for a second and make an important announcement. So as soon as I originally released this video, Dave cainy came in within a couple of minutes and he expressed a concern about one of the things that I'm about to talk about. Now I'm about to talk about setting a couple of environment variables here and showing you how they work. Now
one of those variables is called go route. And setting go route has been shown to cause problems as you move through different versions. Go, especially if you've got multiple versions of the language on your system. So if you want more information about that Dave has a really good blog post here that you can Go to to learn more information about it. But for now, please keep in mind, if you're able to install go at its default locations, which on Unix or Mac is going to be slash user slash local slash go in on Windows is
going to be C colon, backslash go. If you're able to do that, please do that. And then go ahead and avoid setting go route. Now you will need to set the PATH variable to go roots bin directory in order to access the go executable. And you will need to set go path which we're going to talk about after that. But if you can avoid setting the go route environment variable, it's going to save you a lot of heartache. So well, I'm going to show you how to do that in case you do need to set
it please avoid that if at all possible. Okay, at this point, I'm going to resume the video and continue talking about the environment setup. Now the first variable that you're going to need to know about is called go route. Now, if you've installed go to its default location, you're not going to need to worry about this. But if you've decided to install Go somewhere else, for example, maybe you've installed it in your home directory, then you can go ahead and set go route. And that'll tell the environment where to go to find the go binaries.
Now the next thing that we want to do is we actually want to set a PATH variable to the go binaries themselves. So I'm going to go ahead and export a PATH variable. And that'll Of course, start with my existing path. And then I'm going to add on to that go route. And then I'm going to whack on the path slash bin. So there's quite a few binaries that we're going to be using on a regular basis. And those are in go route slash bin, so you're going to want to make sure that that's part
of your path. Now once I do that, let me go ahead and save this. And then I will use the source command in order to get my shell to reread the bash RC file. And then I should be able to test to make sure it goes available by typing go and version. And you can see here I'm running go version 1.8 release candidate three. Okay, now there's one more thing that we need to do in order to get our environment fully set up. And I'm going to go back into my bash RC file in order
to do that. And that is the setting of a second environment variable. So we have go route. And that's going to tell the environment where goes installed. But we're also going to be downloading a lot of packages as we work with go, because we're going to be taking advantage of libraries that other people have published in order to build our own applications out. So those applications as well as our own source code are not going to be located with the go binaries, they're going to be located in a path that we're going to specify with
a another variable called go path. Now go path is either one of the most awesome or one of the most horrible things about go because it gives you this really nice way to specify where you go projects are located. However, it does kind of push you towards having this monolithic repository of all these binaries and your applications tied together. So I'll show you a little bit Of a hint on how to work with that. But for now, let's go ahead and set a go PATH variable in my home directory. And then I will call this
go lib. Now just like with go route, we might have some executable binaries that are going to be stored in our gopath. So I'm going to go ahead and export on our path again. And we'll start with our existing path. And then I want to add on go path slash bin. That way if we install any libraries that have executables, and we will be installing libraries that have executables, then we're going to be able to track that. Now just to show you real quick what that's going to do. Let me go ahead and save this
out resource, my bash RC file. And then I've actually already created this folder here called go live. Now if I go into go lib, and I look at the contents of that it's currently empty. But I can change that by using a tool called go get. So if I get a library that's at github.com slash NSF slash go code. This is historically the library that people use to provide autocomplete functionality in their go applications. So If I go ahead and hit Enter, and wait a second, then go back into my go lib folder. And look,
now I've got some content here. So if I look in the bin directory, I've got this go code executable. And if I come back to the source directory, then I see that I've got this GitHub comm folder. And inside of that is NSF. And inside of that is go code. So if I come in here, here's all of the source code for the go code library. So when I'm working with go code, it actually downloads the source code and compiles it into the go code library for me. And that's what that go path is going
to do for you. The problem that you might run into is that this use of gopath tends to drive towards monolithic repository. So you're going to have go code, you're going to have your own code, you're going to have all sorts of other libraries, all put into this one location. Now when I create my courses, that creates a lot of visual clutter. And so that isn't exactly the form of gopath that I use. What I do is coming back into my bash RC file is I actually use a capability of Gopath to create a compound
gopath. So instead of a single path here, I'm actually going to re export go path and I'm going to add on an additional path to this and I'm going to add Home mic and code. Now a lot of times when I'm teaching courses, I only need to go pants. And so I've got go live. And that's going to be where all my third party libraries go. And then I'm going to have another folder, that's going to be what's called by workspace location. So let's go ahead and write this out. We'll source this again. And now
I've got the full gopath. So if I come into my NGO lib, and I remove everything from it, now it's empty again. So if I go ahead and go get that repository,
[email protected] slash NSF slash go code. And now you see that it actually goes into my go lib folder. If I come into my code folder, which I also created earlier, that's still empty. So the first segment of your go path is going to be used by go get in order to store your files, but all of the segments of your go path are going
to be searched for source code. So that's Going to really help us as we're setting up our workspace. So speaking of which, that's the next thing that I want to do. So a workspace in go isn't anything special, the only thing that you need in order to create a workspace is to have a single directory called SRC in it. So if I add a directory called SRC into my code folder, then I've got to go workspace. Now SRC, as you might expect, is where you're going to keep your source code. So when I set go
path to slash home slash Mike slash code, it's going to look for an SRC directory in order to find my source code. Now, there are two other directories that you might find in a workspace that are interesting. And we found one already when we installed that go code library. And that's been. So anytime we're working with a project and a binary is created, it's going to be put into that bin directory. And that's also why we added that bin segment to our path, the last directory that you might find in your workspace is a pkg
directory. So for Compiling something, and it's going to generate an intermediate binary, which means it's not going to be a fully compiled application, it's going to be an intermediate step. So for example, if we're taking a third party library, and we're integrating that into our application, then the pkg directory is where those intermediate binaries are going to be stored. And the reason those are created is so that they don't have to be recompiled every time. So when you compile your go application, go is going to check to see if any of the source files in
that directory have changed since the last time it compiled them. If it hasn't, then it's not going to recompile that package, it's just going to go ahead and link them into the binary that it's creating for you. Okay, so let's go ahead and clear this out. Because that's getting a little bit cluttered. And now let's set up an editor to work with go code. Now, there's a lot of editors that are out there. And so what I'm going to show you is just one, but feel free to explore the option for your favorite editor, because
There's probably a go plugin for it. And all of these plugins are really good right now. So I'm going to show you the one that I've been using lately, which is Visual Studio code, now might be a little bit surprising Microsoft, oh my goodness, they're doing all this really awesome stuff for the open source community. But it's really true, one of the best development experiences that you're likely to come across with go is in this Microsoft product running on Linux. So I've already installed it. But if you do need to install it, you can come
here to code Visual Studio Comm. It's going to give you the binaries. In this case, I'm running Ubuntu. So I would download this.db file and install that onto my system. And then I'm going to be able to run that by simply typing code in my application launcher, or I've already set up a shortcut over here in my taskbar. So as soon as I run that, I'm going to be presented with this. Now you can See I've already opened up a folder here, you can open up a folder to your workspace by simply file open, and
then pick your folder that you want to be working with. So we're going to be working with code. But there is one setup step that I need to go through before Visual Studio code is quite ready to go. And that is I need to install the plugin that's going to allow us to work with go code. And so if I click this button down here called extensions, then I have a list of all sorts of extensions that I can add in for Visual Studio code. Now, right here is the go extension by Lu COVID. Now
there's a couple of go extensions, but I would strongly recommend you use this one here by Luke, because it is really amazing. It offers a lot of capability, it really makes Visual Studio Code of first class environment for developing go, okay, and as fast as that that's been installed. And it was that fast, because it's been cashed from earlier. And then I'm all set and ready to build my first go application. So let me go ahead into this source directory. And I'm going to create A folder that's going to contain my source code. Now your
first temptation might be to just plug in your source code right here in your src folder. But I wouldn't recommend that the standard structure that you're going to use in a go application is to mirror where your application is going to be in source control. And that makes it go gettable. So in this case, if I was going to keep this file on GitHub, I would create a folder called github.com. And then underneath that, my GitHub account is vi n si Mk II don't ask long story about why I called it that. And then I
would have an application name. So I would call this maybe first app, and that's the folder that I'm going to store my application in. And the reason for that is if you think about if I check this into GitHub, and observatory called first app, when I go get that, it's going to recreate this structure. And so you want to create your applications following that structure. So now I'm ready to create my first file, and I will call that main go. And then I can start adding in my source code. So the first thing that I'm
going to add is package main. And then when I save this, oh, it looks like I expected something to happen here. And it's not happening. I think it's because yeah, he told me that I needed to reload the environment. Good. So I'm going to go ahead and do that. So I'm going to exit out of Visual Studio code, and it should kick right back off, but it has to initialize the plugin. So now I see what I was expecting to see. And that is that the NGO plugin has recognized that they don't have all the
tools that it needs in order to provide me all the support that it can, because the NGO plugin for Visual Studio code is actually like a lot of the plugins and other languages, it takes advantage of these language services, for example, it's talking to me about go land, go lint isn't available on my system. And so it's not going to be able to provide linting capabilities. So basically, the go plugin, cause up to all these language services. The nice thing about that is if you decide to flip between different editors, your experience Is pretty much
the same, because they're all relying on the same language services in order to provide you the capabilities that you have something go ahead and install all. And you can see there's a pretty long list of libraries that it's installing for me. Okay, and now they're all installed. So that took about a minute to install all those dependencies. So I expect that you'd probably have a similar experience in your own environment. Now, before we start adding anything else, I actually put some quotation marks around this package, that's not going to be correct. And now I'm ready
to start actually building out my program. So let me go ahead and put in an import statement here. And we haven't talked too much about imports. But we have mentioned a little bit about packages, packages, or how code is organized into sub libraries inside of NGO. So for example, if I want to build a web application, then I might pull in the net HTTP package that you see here, in order to set up my web request handlers. But for now, I just want to do a Simple Hello, go example. And so I'm just going to
import the FMT package. But you notice that the NGO plugin for Visual Studio Code gives me autocomplete, so any library that's available on my go path is going to be found here. So now I'm going to create a simple function called main. And inside of that, I'm going to access the FMT package. And notice that I get autocomplete functionality here. So I can go ahead and say I want to call the print ln function, and it gives me the signature for that function. So I can go ahead and add that in here. And the I
just want to say hello, go. Okay, so in order to run this, inside of Visual Studio code, you do have the ability by pressing Ctrl backtick, you can open up a terminal right here inside of the editor. And there's a couple of different options that I have in order to run my application. So the first thing that I can do is I can use gos run command, as you see here, and I can give the path all the way through to my source code. So if I come and I just keep tapping through, then I'm
going to get source Slash github.com, slash vianne, si MKE slash first app slash main.go. So if I run that, then it will compile that temporarily, and run that for me. And it also compile in any third party libraries. So the FMT package was compiled in as well. Now, that's a really good way to get a really quick run. Another way that you have available to you is to use go build and go build takes the actual package path. So all we're going to do here, is compile the first app package. Now, if it finds a
main package with a main function, then it's going to compile that as an executable, like you see right here in my home directory. So I can go ahead and run that. Now the last build tool that I have available is go install, go install is actually expecting to be pointed to a package that has an entry point. And it's going to install that into your bin folder. So let's go ahead and see that work. So we'll go to github.com, my username for GitHub, and then first app again. So notice I'm using the package address, I'm
not using the folder path. If I run that, notice, I don't get anything In my main directory. But if I come into this bin folder here, now I've got first app over here. So if I come back to my terminal, bin slash first app, run that I get Hello, go printed out again. Okay, so the last thing that I want to show you if I come back over to the terminal, so you see how we have all the packages that we're working with locally over here in this code directory. If I come to the first
element of my go path, which is go lib, and look at that, you'll see that this is starting to become a pretty busy place here. Because now I've got three source folders. And if I come into GitHub comm, you'll see that I've got quite a few packages. So now all of these third party dependencies aren't cluttering up my main workspace. They're all in this go lib folder, and then I can focus on my development in my code folder. Now the last place that you're going to see packages is over in the NGO installation directory. So
if I look in that folder, you see here that I have this directory called source. So the ghost source code itself is in fact A valid go workspace. So if I come in here look If the folders in here, you see, these are exactly the go standard library. So here's FMT. Here. You see if I scroll down a little bit the net package, if I go into the net package, you'll see that that contains the HTTP package. If I come into the HTTP package, and list those contents, you see all of the source codes for
all of the modules that you can see over here at go. Lang Comm. So if I follow that through, over here, scroll down to net, and HTTP, you see all of the capabilities that are available in here? Well, all of those are provided by this source code here. So if you have any questions about how any one of those libraries work, or how something's configured, you can jump right into the source code and see how it's all put together. Over the last couple of videos, we've laid some foundation by talking about the history and the
features of go and working through setting up the local development environment. What today is the day that we're going to start a discussion about the go language itself By discussing how to work with variables. So in order to fully cover how to work with variables, there's several topics that we're going to need to cover. We're going to start by learning how to declare variables. Then we'll move into a discussion about how go considers read declaration variables. And this concept of shadowing. After that, we'll talk about visibility, where we're going to learn how we can control
what aspects of our program can see a variable that we create, then we'll talk about naming conventions. And finally, we'll wrap up with a brief discussion about how to convert variables from one type to another. Okay, so let's go ahead and get started. So I'm going to be using the playground in order to host our conversation. That way, you can follow along whether you've set up a local development environment, or just want to follow along online. So as you can see, when we first load up the playground, we've got this one statement program, which is
just printing out the string hello playground to The bottom of the screen, when I click this run button here. Now this statement can only ever do one thing because we're passing in a string literal. And similarly, if I pass in the number, say 42, and run it, then we print 42 out. Now no matter what we send to this print ln function, it's only going to ever do that one thing. So in order to provide a little bit more flexibility to our application, we're going to introduce variables. So there's actually three different ways to declare
variables and go and we're going to go through each one of those. And then we'll talk a little bit about where you might use each format. So the first thing that we can do is actually declare the variable itself. And that'll be done using this kind of a statement. So we're going to start with the var keyword, then we're going to have the name of the variable and the type of the variable. Now if you come from another strongly typed language, this might look a little bit backwards, because you might be expecting to see something
like int i. Well, that's not actually how go works. And if you think about it, the way gos structures, things actually looks more like how you read it. So I'm going to declare a variable called I that's going to be of type integer. So the way you declare variables and go, it's pretty much the same way that you speak. And so it might look a little bit odd when you first start working with it. But it very quickly becomes intuitive. Now that we have our variable declared, we can go ahead and assign a value to
it. And we'll do that simply with the equals operator. So once we have that set, we can go ahead and replace this with an AI, run it and we print the value 42 out. And since variables in go can vary, we can go ahead and change the value of that to say 27. When we run again, we get the value 27 printed out. So now even though the statement can only ever do one thing at a time, the program that runs up above this FORMAT statement can actually influence the value that's printed out to the
console. Okay, so let's go ahead and get rid of this, and then explore another way to initialize this variable. Because one Of the things that we trying to do and go is we want to keep things simple. So if we need multiple lines, then we'll go ahead and do that. But we don't want anything to be more verbose than we have to. So we can actually combine these two lines in one like this. So we can initialize the variable i as an integer and assign the value 42 in the same line. So if we go
ahead and run this, then no big surprise, we get the value 42 printed out. Now, this is actually still making us work a little bit harder than we need to. Because since we're assigning the value 42 to this, the go compiler can actually figure out what data type it needs to have. So we can go ahead and tell it to figure this out for us by replacing all this text with this text here. So if we just say I and then use this colon equal operator and the value 42, and run this, then we get
this really nice concise way of setting things up. Now, where are you going to need each one of these? Well, let me go ahead and add them back in so we can talk about them. So we'll set var I integer I equals 42. And then we'll use var j integer equals 27. And then we'll set up K and that's going to be equal to 99. So we have these three different formats. So when are you gonna want to use each one of these? Well, the nice thing about this first format is there are going to
be times when you want to declare a variable but you're not ready to initialize it yet. So for example, if you want to declare variable in the scope of this main function, and then you have a loop or something like that, that sets up a local variable scope. And that's where the variable is actually going to be assigned, then you can go ahead and use this first syntax. The second syntax is valuable, if go doesn't have enough information to actually assign the type that you really want assign to it. So to show that, let me
go ahead and add another print statement here. And I'm going to need to come up these other lines out in order To make things happy for us. So let me go ahead and change this print ln to a print F. And what this is going to do is it's going to allow us to provide a formatting string and print things out that way. So we'll print out the value, and then I'll print out the type of the variable, and then I'll pass in j twice. And so if I run this, then we get the value
27. And no big surprise, it's an integer. But what if we want this to be, for example, a floating point number? Well, with this syntax, it's as simple as just changing this to float 32. When we run this, then go understand that we want to use 27 as a floating point number. Now, if we tried that with K, down here, so let me uncomment the K, switch this over to use K and run, then you see that the number 99 is inferred to be of type integer. And we can influence that a little bit by
adding a decimal point. And that's going to give a hint, making that a float 64. But there's no way to use this colon equals syntax to initialize a float 32. For example, go is either going to decide it's an integer type, Or it's a float 64 type. So if you need a little bit more control than the second declaration, syntax is going to be valuable for you. Now the next thing that I want to show you is how we can declare variables. Now we've been declaring variables one at a time and inside of a function.
Well, another way that you can declare variables is up here at the package level. Now when you're doing it at the package level, you cannot use this colon equals syntax, you actually have to use the full declaration syntax. So we can declare a variable is an integer set equal to 42. So that works, come down, wipe out this code, and then replace this with I. And you see that we have the value 42. And it's of type integer. So just like we have before, we can go ahead and tell it to declare that as a
float 32. And the compiler recognizes, well, I can make 42 a floating point number, that's not going to be a problem at all. Now, of course, if you try something like this, the compiler has no idea how to convert the string food to a floating point number, and so it's going To fail on you. Another thing that we can do at the package level is we can actually create a block of variables that are declared together. And to show you why that's valuable. Let me just drop in some variables that we can play around with.
Okay, so as you can see, I've got a little bit of Doctor Who has a brain here, and say I'm writing a program that's going to print out some information about the doctor's companions. So here we got a variable actor name, that's going to be Elisabeth Sladen, then we got her companion name, which doctor she worked with, and what season she was on. So by declaring these variables like this, things are actually a little bit cluttered. Because again, we want things in go to be as clear and concise as possible. So all these var keywords
are actually cluttering things up a little bit. So what we can do instead of this is actually wrap this whole section with a VAR block. And then we can actually get rid of the use of this var keyword. And now all of these variables are going to be declared because they're inside of this Var block. And we can actually show that they're related somehow. Now they don't have to be related. That's a design decision. But we can do that. So if we had another set of variables that were related to a different context. So say,
for example, we had a counter, and that was going to be an integer initialized to zero, then we can have multiple variable blocks at the package level. And that's just going to allow you to organize your application a little better, and keep your code a little bit cleaner. Now the next thing that I want to show you and let me just drop in some code here is how variables work when you're trying to read Eclair them. So you can see in this application, we're declaring the variable i up here in the package scope, then I'm
declaring it here inside of the main function, and then I'm re declaring it here on line 11. Now, if I try and run this, I'm actually going to get an error. And the error comes on line 11 here, because there's no new variables here. So I can reassign The value of i 13. But I can't declare a new variable here. And that's because the variables already declared on line 10. And you can't declare the variable twice in the same scope. However, if I get rid of this line, notice that the application runs just fine, and
uses the value 42. So even though I is declared twice in my application, once at the package level, and once inside of the main function, that's okay. And what happens is that the variable with the innermost scope actually takes precedence. So this is called shadowing. So the package level is still available, but it's being hidden by the declaration in the main function. And I can actually show that by copying this line up here and running this. And now you see I get 27, which is the package level scope. Then I create the shadow Variable setting
equal to 42. And when I print out I again, then I get the new value of i. Now another interesting thing about variables in go is that they always Have to be used. So let me just drop in this example here. And let's walk through it. So I'm declaring a variable, I'm setting it equal to 42, then I'm instantiating, a variable j, setting it equal to 13. But I'm only using i. So what happens when I run this? Well, if I do, I actually get yelled at, because j is declared and not used. And this
is one of the things that's going to keep your go applications nice and clean. If you have a local variable that's declared and not used, then that's actually a compile time error. And the reason that's really valuable is as your application grows and evolves, and new features are added and old features are deprecated, you're very likely to end up with some old code hanging around inside of your functions. Well, if any of your old code or variables and those variables are no longer used, then the compiler is going to detect that for you see that
you can make sure that you can clean those out. Now another important thing to know about when you're working with variables is how to name them. And there's actually Two sets of rules that you're going to need to keep track of. One is how naming controls visibility of the variable and go. And the other is the naming conventions themselves. So notice that I've been creating lowercase variables. Well, that actually isn't always the case in NGO, because if I'm declaring a variable, and let's just declare it at the package level, and I declared as an integer,
with the name I, and I go ahead and work with that, then that lowercase variable name actually means that this variable is scoped to this package. Now, this is a main package. And so this doesn't really matter so much. But when we get into working with packages, this becomes extremely important. So lowercase variables are scoped to the package, which means anything that consumes the package, can't see it and can't work with it. But if I switch to an uppercase letter, then that's what's going to trigger the go compiler to expose this variable to the outside
world. So it's a very simple naming convention. And there's really only three levels of visibility For variables in go. If you have it at the package level, and as lowercase is scoped to the package, so any file in the same package can access that variable. If it's uppercase at the package level, then it's export at the front of package and it's globally visible. And the third scope is block scope. So when we have this main function here, it's actually establishing a block scope. So when we declare this variable I write here on line 10, that
variable is scoped to the block. And so that's never visible outside of the block itself. Now beyond that, it's important to understand the naming conventions in go, and there's a couple rules that we need to follow. The first is that the length of the variable name should reflect the life of the variable. So for example, in this example, we're declaring a variable i, and we're using it right away. So having a very simple variable name is perfectly acceptable. And this is going to be especially true if you're declaring counters and for loops and things like
that, it's very common have single letter or very, very concise variable Names, because the lifespan of that variable is very small. And the amount of time that you have to keep the meaning of that variable in your head is very small as well. However, if you're declaring a variable that you use for a very long time, then it's best practice to use a longer name. So for example, if this was going to represent the season number that our companion was on, so say, season 11. And we use that throughout this entire main function, then we'd
want to use a name something like season number. Now, if you're working with a package level variable, and that package level variable is going to be used in quite a few other places, then that's where you're going to want to use the most verbose variable name. Now, you still shouldn't get crazy, you shouldn't have 50 character long variable names if you can avoid it. So keep your names as short as you can. But make sure that the names of those exported or package level variables are clear enough so that somebody who's outside of that source
file understands the meaning of it. The other thing I'd like to talk about Is how to work with acronyms, because in other languages, you might see variables like the URL, and then maybe this is http google.com. You might see a variables name like this. Well, the best practice and go is actually to keep these acronyms as all uppercase. So if you're working with a variable called the URL, the URL should be all uppercase. Similarly, HTTP and any variables like that. So anytime you see an acronym, make sure that that's all uppercase. And the reason is
for readability, it's very clear, we're used to seeing URL and HTTP all put together. So when you read this variable, it's very clear that you're talking about an HTTP. Maybe you're talking about an HTTP request that reads a little bit cleaner than if you go ahead and make those lowercase. So just some rules to keep in mind as you're creating variables. Now the next example that I want to show you is how we can actually convert from one variable type to another. So notice that I have two variables here. I've got variable i on line
Eight, and then I'm declaring as an integer with the value 42. And then I've got this variable j, that's going to be a floating point number. Now what I want to do is I want to actually treat it as a floating point number and assign that value to J. So the way that I do that is using this conversion operator. So if you look on line 12, it looks like float is being used as a function. And in fact, it is. And this is a conversion function. So when I run this program, you see that
the first print statement on line nine, prints 42, as an integer, the second print statement still prints the same value 42. But now it's been coerced into being a floating point number, they have to be a little careful with this. Because if you go the other way, so for example, if we go from a float 32 to an integer, and then I convert that and run it, it looks like everything's okay. But keep in mind, a floating point number can have a decimal on it. So now I've actually lost information to the conversion. So the
important thing about this is I have to explicitly convert, because if I just tried to do this, then I'm actually going to get a compile time error, because go is not going to risk the possibility of losing information through that conversion. So you have to do an explicit conversion, when you're changing types. That way, it's your responsibility to understand if you're losing information or not. Now, the other thing that's important to know is if I decide to work with strings, it's a very common use case, to try and convert an integer into a string, say,
for example, you want to print it out to a log file. Well, if I run this, I get a pretty odd result. The first line prints out Okay, 42. And that's an integer, that's okay. But then I get an asterisk. That's of type string, what the heck happened there? Well, in order to understand that, you have to understand how strings work with go, a string is just an alias for a stream of bytes. So what happens when we asked the application to convert the number 42 into a string is it looks for what Unicode character
is set at the value 42. And that happens to be an Asterisk. So if you want to convert back and forth between strings and numbers, then you're actually going to need to pull in the string conversion package, which you can find on go Lang under packages. If you scroll down a little bit, you see string conversion here. And this is going to expose all sorts of functions that are going to make it a lot easier to convert back and forth between strings and other data types. So in this case, what we'd want to do is
use the string conversion packages I to a function, which converts an integer, that's the I two, that's the two and then to an ASCII string. So if we go ahead and run that, now you see that it properly converts the integer 42 into the string 42, and prints it out for us. So if you need to work with converting between numbers and strings, then go ahead and use that string conversion package. But if you're converting between numeric types, just keep in mind, you can't implicitly do that conversion, you have to explicitly do it. And if
you need to do that, then you can go ahead and use the type as a function. Okay, So we just covered quite a bit of ground. So let's go through a summary of what we've talked about throughout this video. Okay, there's quite a few things to keep in mind as we're working with variables. So let's go through and review what we talked about. The first thing that we talked about is the three different ways to declare variables. So we saw that we could declare a variable, and then initialize it later, we saw that we can
declare it and initialize it at the same time. And then we also saw that we can use this colon equals syntax as a shorthand version of declaring and initializing a variable. And then we let the compiler decide what type to assign to that. Now normally, you're going to use this third version, the only time you're really going to use the second version is when the compiler is going to guess wrong. And then the first version is sometimes useful. If you need to declare the variable in a different scope than you're actually going to initialize it.
We then talked about how we can't read Declare a variable. So within the same scope, we can't initialize that variable twice, but we can continue to reassign values to it, but we can shadow them. So if we declare a variable in a package scope, for example, we can read declare that in a function scope, and that's going to shadow the variable at the higher level of scope. All variables must be used in a go application. So if you declare a local variable, and that variable isn't used in the scope that it's declared or one of
its inner scopes, then that's going to trigger a compiler error. And you're going to have to go back and clean that up before your application is going to run. And again, the reason that that's really nice is as code evolves, and it continues to be refactored and improved over time, and features get retired, you don't have all these old variables hanging around and requiring allocations of memory when they're not being used for anything anymore. We also talked about the visibility rules. The first thing that you need to know is when you're working with package level
variables, A lowercase first letter means that it's scoped to the package, which means all of the source files that are in the same package have access to that variable. If you have an uppercase first letter, then it's going to be exported globally, and so anything can work with that variable at that point. There is no private scope. So you can't scope it. variable to the source code itself. However, you can scope a variable to a block by declaring it within the block instead of declaring it at the package level. We also talked about the naming
conventions. And there are really two naming conventions that are used Pascal case, which basically means you uppercase, the first letter, and camelcase. So when you're naming variables, you don't want to separate the words in the variables with underscores. You don't want to have them all in lowercase, you don't want to do anything like that. Just use standard Pascal or camel casing rules. The only exception to that is if you're working with acronyms, all of the letters in the acronym should be uppercase. The names of the variables should Be as short as you reasonably can get
them. So you've got a very short lifespan of the variable, say, for example, a counter in a for loop, then having a one letter variable name is perfectly acceptable. However, if you've got a variable that's got a longer lifespan, say, for example, it's used in a fairly long function, or if it's exported from the package, then having a longer more descriptive variable name is certainly something that you should consider. However, please don't go crazy. Keep those names as brief and concise as possible. And the last thing we talked about are the type conversions and how
these work a little bit differently than other languages, a lot of other languages, you would have to put the type in params, and then what you want to convert, in NGO, it acts more like a function. If we want to convert an integer to a floating point 32 number, then we use float 32 as a function, and we pass the integer into it, and it's going to do the type conversion for us, we also learned that go does not do implicit Type conversion. So if you try and take a floating point number and assign it
to an integer, goes going to throw a compile time error for that. And that's because of the possibility of losing information through that conversion. So every time you're going to do a type conversion that might lose information, you're going to have to do that yourself so that you're making the decision. And then you can write whatever tests are required in order to make sure that information hasn't lost. The final thing that we learned is when we're working with strings, then type conversions can start to get a little bit weird. So in order to handle the
conversion between integers and strings, and other data types in strings, we can use that string conversion package that offers a series of functions that make sure that the conversions happen the way that we expect them to. In today's video, what I want to do is introduce the primitive types that we have available in the go language. Now, we're not going to be talking about every basic type that you can create. There are Certainly collections and some more complicated types. And we'll introduce those a little bit later. Today, I want to focus on three categories of
information that we can store and go. We'll start by talking about the Boolean type, then we'll move on to the numeric types. And in that category, we have integers, floating point numbers and complex numbers. And then we'll move on to the text types. Okay, so fairly simple agenda, but we've got a lot to cover. So let's go ahead and get started by talking about how we can work with boolean data in go. boolean data is probably the simplest type of data that we can work with and go. And it represents two states, you either have
true or you have false. So in order to show you a simple example of working with Boolean variables, we can create a variable here and make it of type bool. So that's the data type that you're going to use when you're declaring a Boolean. And we can set it equal to A values. So for example, we can set it equal to true, and then we can go ahead and print out using our Fancy printf statement here, we can print out the value and type of this Boolean. And if we run that, we see that the
Boolean true is in fact got the value true. And its data type is Boolean. So we can also initialize this to false, and run that. And now we see false is also a Boolean. Now there's a couple of uses for Boolean variables in our applications, perhaps one of the most common is to use them as state flags. So for example, say you're creating an application and you want to store whether a user is signed up for notifications when a report is generated. Well, you can use a boolean variable in order to store true if they
want that report or false if they don't. The other case. And perhaps the more common case for using Boolean and go is as a result of logical tests. Now, we're not ready to talk about logical tests quite yet. But I can show you how Boolean 's are used in those tests. So if we create a simple variable here, and we use the equals operator to test if one equals one, and then create another variable, and then I want to test if one equals Two. Now, the double equals operator is called the equals operator. And that's
basically checking to see if the item on the left is equivalent to the item on the right, so one obviously equals the number one, and one, just as obviously doesn't equal the number two. So if we print out the value of those using these two printf statements, then we see that a Boolean is actually generated as a result of this equivalency test. Now when we get into talking about logical tests, we'll see that there are actually quite a few other logical tests that we can use. But this is a very common use case that we
have. So we see looks like I need to add a new line operator here, we see that the first operation does in fact, generate the Boolean true and the second operation generates the Boolean false. Now the other thing that's important to know about the primitives is that every time you initialize a variable, Ingo, it actually has a zero value. So we're assigning the value of n and m in this example here, but what happens is If I just do this and print out The value of that, well, in some languages, that would be uninitialized memory,
and we would have no control over what printed out, when go, every time you initialize a variable, it has a zero value in the zero value for Boolean is the value false. So you don't have to worry about initializing variables every time. If the zero value is acceptable to you, you can certainly leave it like that. So Boolean is a pretty simple. The next type that I want to get into are the numeric types. And these are a little bit more complicated, because of all of the different types of numbers that we can work with
in our applications. Go has a rich array of numeric types to choose from. Now, the first thing that we need to know about is the zero value. And the zero value for all numeric types is going to be zero, or the equivalent of zero for that numeric type. So let's start talking about the integer types. So the first type of integers that we can work with are the signed integers. And those have several different data types. So we have the general int, which is an integer Of unspecified size. And I say unspecified because every platform
can choose to implement it as a different size. Now, the one thing that you're guaranteed is regardless of your environment, and it will be at least 32 bits, but it could be 64, or even 128 bits depending on the system that you're running on. And this is going to be the default integer type. So if we do something like n equals 42, and then we print out the type of n, then you see that we get the value 42. And it's of type integer. Now, there are other types. So we can have eight bit integers,
which can range from negative 128 to 127. We can have 16 bit integers, which can go from negative 32,007 68, up to 32,007 67, then 32 bit integers, which can go from approximately negative to positive 2 billion. And then if you need a really big number, you can go with 64 bit integers. And those go somewhere between plus and minus nine quintillion. So if you need bigger numbers than that, then you've got a very large application that you're working with. And in that case, you're going to need To look into the big package from the
math library, which can handle arbitrarily large numbers. So you can't get a number big enough for the big package to not be able to handle. Although working with numbers at large, you're going to take a bit of a performance hit and the numbers aren't going to be quite as easy to work with is using the primitive types that we're talking about here. Now related to the signed integers are the unsigned integers. So we have the value 42 here, and just so you can see, we can create an unsigned integer. And I'll just pick at you
and 16 and assign that the value of 42. And then let's go ahead and run that. And you see that now we have a un 16. So there's an equivalent type of unsigned integer for every signed integer, we have you int eight, which is an unsigned eight bit integer which can go from zero to 255. We have a 16 bit unsigned integer and a 32 bit unsigned integer. Now, what we don't have is we don't have a 64 bit unsigned integer. But we do have a type byte. And a byte is an alias for an
eight bit unsigned integer. And the reason we have that is because the unsigned eight bit integer is very common, because that's what a lot of data streams are used to encode their data. Now with these integer types, and again, unsigned unsigned integers are basically the same type, we've got several different arithmetic operations that we can do. And these are built into the language. So if I just dropped in this example, here, you see that we've got an integer, a set equal to 10. And we've got another integer be set equal to three. And we're doing
the basic arithmetic operations that are available to us. So we can add, we can subtract, we can multiply, we can divide, and then this percent sign here is actually used for the remainder. So if I run this, you see that we get 10, plus three is 1310, minus three is seven, and so on. So we get the numbers that we expect, then the one that you might not expect is this a divided by b. So 10 divided by three Is three, remainder one. And so we get the result three out, because when you divide an
integer by an integer, you have to get an integer result. So the type cannot change during the operation. So when we do this, we're doing what's called integer division, and we dropped the remainder. Now, if the remainder is interesting, that's what this remainder operator is for. And then we can pick up that remainder one out of it. Now, just like when we're doing division, we can't change the type. So dividing an integer by an integer can't give us a floating point number, for example, we're also not allowed to add to two integers of different types.
So if we take this as an example, we've got the integer, a set equal to 10, and an eight bit integer set equal to three. And if we try and add those together, we're actually going to get an error. So even though go might be able to do that, theoretically, it's very, very insistent that it's not going to work across types without your consent. So in order to make this work, we would actually have To do a type conversion on one of the variables to convert it into the type of the other. So just like
we talked about in the last video, where go is very, very hesitant about implicit data conversion. This is another example. Even though these integers are almost equivalent, go is not going to make that assumption for you, you're going to have to do the type conversion. Now a couple of other operations that we have are called the bit operators. So if I drop in this example, we see the full Bit operators that we have, we've got the AND operator, we've got the OR operator, we've got the exclusive OR operator, and we've got the and NOT operator.
Now what's going to happen when we run this? Well, let me just run this first and get these results. So you see that if we take a and b, then we get two, if we take a or b, we get 11. And these might not make a lot of sense. So in order to clear things up a little bit, let me put in the binary representation of these. And then we can walk through what these are doing. So 10 is 1010, in binary three is 0011. Now when we run into an end Operation, that's going
to look for what bits are set in the first number and the second number. So as we can see, we've got four bits in each one of these numbers that are allocated. Actually, these are 32, or 64 bits long, but I'm ignoring all the zeros at the beginning of these numbers. So let's look at the four digits that matter. So if we look at these, then we see actually, if we add these together, we're going to get 0010. And in binary, that is two, so 10, and three equals two. Now if we do the or,
or means if one or the other is set, so we get one, because a has the first bit set, neither one has the second bit set, both have the third bits set, so we're going to include that, and then B has the last bit set. So we'll have that. So now we end up with 1011, which is one plus two plus eight, which equals 11. Exclusive OR means either one has the bit set, or the other does, but not both. So in that case, we're going to do 1001. The only difference between this and the
OR operation is that third bit where they're both set to true. And therefore we're Not going to include that. Now in the end not that's kind of the opposite of or because within not, it's going to be set true only if neither one of the numbers have the bit set. So since the first bit is set in a, we're not gonna include that, neither one has the second bit set, so we're going to include that both have the third bit set, so we're not going to include that. And B has the fourth bit set. So
we're not gonna include that. So we get 0100, which is equivalent to the number eight. And that's how these bit operations work. The last example that I have to show you with integers is what's called bit shifting. So when we have this example here, the first print statement is going to bit shift a left three places. And the second is going a bit shift a right three places. So let's run this and see what that's going to do. And so we get to number 64, and one, so in order to understand that, let me go
ahead and put in these values, so we can understand what's going on. So eight is really two to the third power. And when we do bit shifting, we're Basically adding to that exponent, as long as we're dealing with the power of two, because really, what we're going to do is we're going to take this to to the third and multiply it by two to the third, which is equivalent to two to the sixth, and two to the six is 248 1632 64. So that's how we get the 64. Now, when we bit shift to the
right, we're going to take our original number, and we're going to divide it by two to however hard we're shifting, so we're going to divide it by two to the third. And in that case, we're going to get two raised to the zero power. And any number greater than zero raised to the zero power is one. And so that's how those operations work. The next data type that I want to talk about are the floating point types. So we have a lot of different integer types. So we can store a lot of different size numbers.
But with integer types, we can only store integers, so they can be positive or negative integers or zero, but we can't store decimal numbers. So in order to store decimal numbers in go, we're going to use the floating point numbers. Now the floating point numbers in go follow I triple E 754. Standard. And in that standard, we're going to pull out two of the types. So we've got 32 bit floating point numbers and 64 bit floating point numbers. So if you're working with a 32 bit floating point number, you can store numbers between plus or
minus 1.18 times 10 to the negative 38, all the way up to 3.4 times 10 to the 38. So from very small numbers to very large numbers. If you need even more precision than that, then you can use a float 64. And that can go from plus or minus 2.23 times 10 to the negative 308th, all the way through 1.8 times 10 to the 308. So how do we create floating point numbers? Well, here's some examples of how we can do that. So line eight here shows you how you're going to define your flooding point
literals almost all the time. So we're going to declare a variable and set it equal to 3.14. And away we go. This next line here shows that we can use exponential Notation. So we can use 13.7 times 10 to the 72nd. And that's going to be able to use the short form iE 72nd to stand for that 10 to the 72nd. So if I run this, we're actually going to get the final result here, of 2.1 times 10 to the 14th as a floating point 64. But notice we didn't get any errors. So all three
of those declarations, syntaxes are okay, so that's how we can work with floating point numbers and show you how you can explicitly declare these We can use, let me just do var and float 32, for example, and initialize that. And that's how you're going to declare a floating point number. Now unfortunately, this number is a little big, because we can only go times 10 to the 38th power. So if I comment that line out, things are going to run properly. If I come back in and make this a floating point 64, then we can restore
this number. And that's another thing that's important. If you're going to use the initializer syntax on a decimal, it's always going to be initialized to a float 64. So keep in mind, you can't do arithmetic operations between Float 64 and float 30 twos. So if you're just using the initializer syntax, you're going to want to make sure that everything's working as float 64. And if you forget, don't worry about it, the compiler will complain at you, and you can quickly go in there and make sure that everything's working properly. Okay. Now, speaking of arithmetic operations,
let me jump in a couple of those. And you can see the arithmetic operations that are available with floating point numbers. So if I run this, we get the expected answers of adding, subtracting, multiplying dividing two numbers together. Now, a couple things to notice here, when we divided A by B, we did in fact get a decimal result, because as long as we're working with floating point numbers on both sides, we can get a floating point result. As a matter of fact, we have to get a floating point result. The other thing to notice, we
don't have the remainder operator available that is only available on the integer types. Further, we don't have the bitwise operators or the bit shifting operators. So if you need to work With those, you're going to have to work with the integer types. The last type of numeric primitive that we have available in go is the complex type. And this is really kind of exciting because this is fairly rare in the languages that I've worked with, where complex numbers are actually treated as a first class citizen, and it opens up go to be used as a
very powerful language for data science. So if we come in and paste an example, you can see a very basic declaration of a complex number. Now there are two types of complex numbers. There's complex 64, and complex 128. And the reason we have that is we're basically taking a float 64 plus a float 64, or a float 32 plus a float 32 for the real and imaginary parts. Now here, I've got a very simple complex number, that's one plus two I, if I go ahead and run that, you see that in fact, it prints out
as one plus two I and that's complex 64. So goes parser understands the I literal as an imaginary number, and it uses that when you're creating your variable. Now we can actually go even simpler than that, Because AI is considered special. And we can run this with just two AI and we get zero plus two I down here and the result. Now what operations that we have available. Well, we can do addition, subtraction, multiplication, and division again. So I've got two complex numbers defined here. If I run this, then I get the expected result, where
the real parts are added together, and the imaginary parts are added together, or subtracted, multiplied, divide whatever operation we're applying. Now, what happens if you need to decompose this down. So if I come back to our first example, here, where we have one plus two I, not every operation that I'm going to do with these numbers is going to need this to work as a complex number. So what happens if I need to get at the real part or the imaginary part? Well, in order to do that, we actually have two built in functions in
the language. So let me wrap this in here with a call to the real function. And then I can follow that up with its partner, which is the image function. And what those are going To do is those are going to look at the complex number that you provide. And they're going to pull out the real part or the imaginary part. And these functions work with complex 64 and complex 128. So if you run this on the complex 64, then the real and the image function are going to give you float 30 twos out, if
you run this on complex 128, it's going to give you float 64 is out because those are the data types used for the components. So if we run this, we see that we get float 32 is out. If we convert this to a 128 and run this again, then we're going to get float 64 is out. And it's going to break apart that complex number into the real and imaginary part. So we can work with those however, we need to know the complement of these two functions is the complex function. So if you're working along
in your program, and all of a sudden you need to make a complex number. How do you do that, because you can't use this literal syntax. So in order to do that, we do have another function. And that's the complex function. And this takes two numbers. The first number Is the real part. And the second number is the imaginary part. So let me go ahead and wipe out this line. Get rid of these real calls here, and then run. And now that we see that we can take two floating point numbers. In this case, they're
considered to be floating point 60 fours because we're making a complex 128. And it creates five plus 12 I for us, the last data type that I want to talk about is the text type. And texting go falls into two basic categories. One, I can talk a lot about the other we're just going to touch on. So the first text type that we have available is a string, and a string. Ingo stands for any UTF eight character, so that makes it very powerful. But that means that strings cannot encode every type of character that's available.
For that we need the other text type which we'll talk about in a second. But let's just start by introducing a basic example here. So here I've got a string literal, this is a string, I'm going to print out its value and its type. So if I go ahead and run this, you see that this is a string print out. And It's of type string. No big surprise here. Now one of the interesting aspects of a string is I can actually treat it sort of like an array. Now, we haven't talked about arrays yet. But
I can actually treat this string of text as a collection of letters. So if I do something like this, I'm actually going to ask it for the third letter, because arrays and go are zero based. So I'm going to look for the 012. That's the third letter in the string, which is the letter II. So if I run this, I get an interesting result, I get the value 105. And that's a un eight. So what the heck happened there? Well, what's happening is that strings and go are actually aliases for bytes. So we can go
ahead and convert this guy back, since a byte is just an alias for a string, and we can get our letter I back. Now, strings are generally immutable. So while I can inspect the second character, I can't do something like this, if I tried to run this program, let me just print out the full string here and run this, then I get an error. And there's actually quite a Few things wrong with this. The first thing is I can't assign a string to a byte because I'd have to do a conversion. The second thing is
I can't manipulate the value of the string. Now with the numeric types, I should do that there were quite a few operations that we can perform with it, there is one arithmetic or pseudo arithmetic operation that we can do with strings, and that is string concatenation. Or in simpler terms, we can add strings together. So in this example, I've got the string s and the string s two. And as you can see down in the printf statement, I'm adding s&s two together, and then we're going to print out the value in the type. So if
I run this, you see that it just merges all the strings together, and it gives us the result. Now another thing that I can do with strings is I can actually convert them to collections of bytes, which in go is called a slice of bytes. So in this example, I'm starting with a string, this is a string, and then I'm going to do a conversion to this collection of bytes. And I'm going to pass the string into that. So if we run That, we actually get this as a string comes out as the ASCII values,
or the UTF values for each character in that string. And then you see that the result is a collection of UN eights, which is a type alias for bytes. Now, why would you use this one? It's a very good question. A lot of the functions that we're going to use in go actually work with byte slices. And that makes them much more generic and much more flexible than if we work with hard coded strings. So for example, if you want to send as a response to a web service call, if you want to send a
string back, you can easily convert it to a collection of bytes. But if you want to send a file back, well, a file on your hard disk is just a collection of bytes, too. So you can work with those transparently and not have to worry about line endings and things like that. So while in your go programs, you're going to work with strings a lot as strings. When you're going to start sending them around to other applications or to other services, you're very often going to take advantage of this Ability to just convert it to
a byte slice. Okay, the last primitive data type that we have to work with is called a rune. Now a rune is a little bit different than a string type in go. Because we're a string type represent any UTF eight character, a rune represents any UTF 32 character. Now, UTF 32 is a little bit of a weird animal, because while any character in UTF, 32, can be up to 32 bits long, it doesn't have to be 32 bits long. For example, any UTF eight character, which is eight bits long, is a valid UTF 32 character.
So there's all sorts of tricks that they have to do in the encoding of the characters in order to know whether the character is one, two or four bytes long. So that makes things a little bit tricky to work with and go. Now we're not going to get too deep into this subject, we're just going to talk a little bit about what runes are. And then I'm going to point you to some things that you can refer to if you actually need to work with rooms in your application. So if we look at this example,
here, we're declaring the room a. Now notice The difference here, if we were declaring a string, we would have double quotes. When we're declaring a single room, we use single quotes. But if I run this, I'm going to get an interesting result. Notice I get the value 97. And it's an int 32. Now that might seem a little weird. And the reason for this is because runes are just a type alias for int 30. twos. So we're strings can be converted back and forth between collections of bytes. Rooms are a true type alias. So when
you talk about a rune and go it is the same thing as talking about an integer 32 name, I think, well, that's just because we're doing some implicit initialization here, we're using that colon equals syntax. So let's specifically and explicitly declare this as a rune and try this again. And we get the same result. And again, that's because a rune is an integer 32. Now, you might be feeling a little lost here. So if I've got a UTF 32 characters at how do I work with that. Well, the Answer comes from the go API's. So
if I jump out to go lang.org, come into the packages. And Let me just jump to the strings package. And then I'll show you this. Notice this function here, read rune. So if you're working with a data stream that's encoded in UTF 32, then you have special functions that you're going to be able to take advantage of, that's going to return those values out. So if we do read byte, which is going to read a single character, then we're going to get a bite out and a potential error. But with read rune go is going
to look at the byte stream, it's going to pull off the next room that's available to you the size of that room, and then a potential error. So you're going to have all the information you need in order to re encode that integer 32 that you're going to have back into its UTF 32 character. So things are a little bit more tricky when you're working with runes. And you're going to have to read into the API's for the go package that you're working with, in order to understand how to work with them in your application.
Okay, so that covers the primitive data types that you have to work with and go, let's go into a summary And review what we've talked about in this video. We covered a lot of ground in this video, and I understand that it might take a little bit of time to process this and really understand all the different options that you have for primitive data types and go. Now before we get into the summary, I do want to let you know that a lot of times the default data types that you're given are going to be
perfectly fine. So if you're working with Boolean, you're going to get a Boolean type, if you're working with integers, you're going to get that signed integer type floating point, it's going to give you a floating point, 64, and so on. So you don't have to memorize every single data type. If you need a specific data type, then you can certainly refer to the NGO documentation in order to find out what's available for you. So let's go through and review what we talked about. The first thing that we talked about was the Boolean type of data,
we found out that it can take two values true or false. And it's not an alias for other types. So In some languages, a Boolean is actually an alias for an integer. So you might use like negative one for true and zero for false when go Boolean is its own type. So you can't convert back and forth between integers and things like that, you're going to have to work with Boolean as their own type. We also talked about the zero value for a Boolean is the value of false. And so if you initialize a Boolean
and don't set a value for it, it's going to receive the value false. Then we talked about the numeric types. And the first type that we talked about were the integer types. And that broke down into two different types with the signed integers. And there are two classes, I guess you could say within the sign integer type. There's the INT type, which has varying size, but a minimum of 32 bits. And this is going to be the most common type of integer you're going to deal with in your applications. But if you need a little
bit more granularity, or a little bit more resolution, or control over how much memory is assigned to the integer, then you can go All the way from int eight, which is an eight bit integer all the way up to 64 bit integer, we also have the unsigned integers where the signed integers have a plus or minus and so they can't store numbers quite so large because they have to store a plus or minus bit. The unsigned integers can store larger numbers, but they can only ever be positive. And we have all the way through an
eight bit which we can use the byte or the UN eight, type four, all the way through 32 bit unsigned integers with the un 32. We have various arithmetic operations that we can perform on both integer types. So we can do addition, subtraction, multiplication, division, and that remainder operation. So remember, division with integers is going to give you an integer result. So you're going to lose that remainder portion. So if you need that, you can use that remainder operator to get it. We also have the bitwise operators. So we can do and or Exclusive OR,
and the and not operations. And the zero value for any integer type is going to be the literal value zero. So when you initialize an integer Type, and don't assign a value to it, that's going to be equivalent to the value zero, you're not just going to get whatever was in memory when that variable is initialized, and you cannot mix types in the same family. Now, this is going to be true throughout all of the numeric types. If you have a un 16, for example, and a un 32. And you cannot add those together, you're
going to get a compile time error. The next numeric type that we talked about where the floating point numbers, so they follow the I triple E 754. Standard, there's zero value is similar to the integer types, the value is zero. And we have two different versions, we got 32 bit versions and 64 bit versions that we can work with. And we have several different literal styles that we can use to initialize them. So we can use a pure decimal like 3.14, we can use exponential notation, for example, 13 e 18, or two e 10. And
it doesn't matter if that E is upper or lowercase, or you can do mixed. So for example, 13.7 e 12 is a perfectly acceptable way to initialize that. We do have addition, subtraction, multiplication and division that we can do with floating point numbers. Now we don't have the remainder operation, but the division operation is going to give us a true floating point result. So we're not going to lose our decimal portion. The final numeric type that we talked about were the complex numbers. The zero value of a complex number is zero plus zero I and
they come in 64 and 28 bit versions and the reason for that is the two components, the real and the imaginary component are either going to be floating point 32 or floating point 64. So when you add those together, that's where you get the 64 and 128 bit versions. We have Some built in functions that we can work with. So we can use the complex function in order to create a complex number, we can use the real function in order to get the real component. And we can use the match function in order to get
the imaginary component of a complex Number. Now what the data type that comes out of that depends on the size of the complex number going in. So complex 64 is going to give you a float 32 out from the real in the match function and a complex 128 is going to give you a float 64 out of the real in the match function, we have the same arithmetic operations as we do for floating point numbers, we can do addition, subtraction, multiplication and division. The final category of primitive data that we talked about, or the text
types, and in go, there are really two different text types. The most common one that you're probably going to deal with are strings. No Strings are represented as a collection of UTF eight characters, they're immutable, you cannot change the value of a string after it's been initialized. You can concatenate strings together with the plus operator. And then you can convert them back and forth between a collection of bytes with this square bracket byte syntax and passing in the string that'll convert it to a collection of bytes. And you can convert a collection of bytes back
to A string by using the string conversion. The other type we talked about is a rune and a rune represents any UTF 32 character. Now runes are a little bit more complicated to work with because of the multi step process that it takes in order to encode a character into the UTF 32 character set. So when we're working with runes as a primitive type, really all we're working with is an alias for an integer 32. Today, I want to talk about constants and how you can use them in your NGO applications. Now, there are several
things that we need to talk about with constants. So like we've been doing, I want to break this down into several categories. The first thing that I want to talk about is how we're going to name constants in our NGO applications. Then we'll talk about type constants, followed by a discussion about untyped constants. And we'll talk about the differences between those two, and the options that each one gives us. And then we'll talk about a method of generating constants that are called enumerated constants. And finally, we'll end our discussion by talking About enumeration expressions, which
are going to build upon the concepts that we're going to talk about in that first enumeration discussion. The first thing that I want to talk about is how we're going to name our constants. So all constants are going to be preceded with the const keyword, that's going to let the compiler know that that's what we're trying to work with. Now, if you've come from other languages, you might be expecting that we're going to name our constants, something like this, where we're going to have all uppercase letters and separate the words with underscores. The problem with
that is if we do that and go, then the first letter is going to be uppercase. And as you remember, from our discussion on variables, if we've got an uppercase first letter, that's going to mean that the constant is going to be exported. And we don't always want that. So instead of this, we're actually going to name our constants the same way that we named variables. So if we had a variable that we wanted to call my const, and we didn't want to export It, then we would start with a lowercase first letter, or in
other words, we would use camel casing. And if we did want to export this symbol, then we would simply change that first character to uppercase. Now assuming that we're going to be working with an internal constant, then we're going to switch this back to a lowercase first letter. And then let's talk about how we can create what's called a typed constant. Now a typed constant is created very similarly to a typed variable. So we can start with the const keyword, then the name of our constant, and then we're going to list the type of the
content, and then we can set it equal to a value, then if we want to prove that that worked out the way we expected it to, then we can go ahead and print out the value in the type of the constant. And we will do that by using this printf statement here. And then when we run this, we see that the constant is in fact created. It's got the value 42 that we assigned, and it's got the type that we assigned to it. Now the reason it's a constant and not a variable is it has
To remain constant. So if we tried to do something like this, change this to the value 27, then the compiler throws an error, because we're not allowed to change the value of a constant. Another characteristic of a constant is that it has to be assignable at compile time. So for example, if I wanted to have a constant that represented the sine of pi over two, then I might be tempted to do something like this. I'll create a float 64 constant. And I'll set it equal to the result of the sine function from the math library.
And I'll pass in 1.57, which is approximately pi over two. And then I can run this right? Well, the problem with that is in order to determine the sine of that value, that actually requires the function to execute, which is not allowable at compile time. And so you can't set your constants equal to something that has to be determined at runtime. And that includes things like setting it equal to flags that you pass into your application, when you run, if you're going to do that you can't use a constant to store that value. Now constants
can be Made up of any of the primitive types that we talked about in the last video. So if we have this example, here, we've got an integer constant, we've got a string constant, a floating point constant and a Boolean constant. And if we run this, we see that all of those printout exactly the way that we expect. We've got the integer the string, the floating point value and the boolean value. Now in an upcoming video, we're going to talk about the collection types and the collection types. are inherently mutable. So for example, you couldn't
create an array and declare that to be a constant type. Arrays are always going to be variable types. Now another characteristic that constants have in common with variables is they can be shadowed. So if we create a constant at the package level, and let's just make this an integer 16, and set it equal to the value 27, then we'll delete these guys. And also these guys. Now we've got a constant called a declared at the package level, that's an integer 16. And then we got a constant in the main function, that's also called a and
It's an integer type. So if we update This printf statement to print the type of variable, we see that the looks like I need to print my variable twice, we see that the inner declaration of the constant wins. So not only can we change the value of the constant, but we can also change the type because the inner constant shadows that outer constant. And we can prove that by commenting this line out, running again. And we see that the package level constant wins. So you want to be a little careful here, because if you're going
to reuse constant, it's going to feel like those values are changing. So I wouldn't recommend that you take advantage of this. But if you do get into a situation where constants aren't evaluating the way that you expect them to, this is one possible reason. Now when we're working with constants, they work very similar to variables when we're using them in operations. So if we bring this line back, and set it equal to 42, and then what I want to do is declare a variable. So we'll declare a variable b as an integer, and set that
equal to the value 27. And then We can do a plus b. And let's see what happens when we do that. So if we run that, we in fact, get the ability to add a constant to a variable, and the result is going to be a variable. And so since the constant and the variable are of the same type, we can perform the addition operation on there. Now, our constant is of a different type. For example, if we made variable b in 16, and run this, then we get exactly the same failure that we get
when we try and add two variables of different types together. Now, so far, all we've been talking about are these type constants, we're after the constant name, we list the type. But we don't have to do that, we can use the compilers ability to infer the type for us. So let's just go ahead and do that with this example here. When we run this, we see that the constant a is inferred to be an integer with the value 42. Now given that, given that the compiler is inferring the value, what do you think is going
to happen? If we do something like this, if we restore that previous example, where we're Going to add this constant to an integer 16? Well, in fact, in this case, the operation succeeds, which might be a little bit confusing. But the reason that works is because what the compiler is actually doing, when it sees this constant is it's basically replacing every instance. So the way the compiler sees this program is it sees it like this. So since we're taking a literal 42, and adding an int 16 to it, that 42 is interpreted as being an
integer 16. So the compiler doesn't say, oh, constant, a equals 42, that's an integer and always an integer. Instead, the compiler is going to look for every time that we use the symbol a, and it's going to replace that with the value of the constant. And so we can do these implicit conversions when we're working with constants, which is something that we can't really do when we're working with variables. The next thing that I want to talk about are what are called enumerated constants. So let me go ahead and start that conversation out by wiping
out what we have here, clean up our code just a little bit. And then I'm going to do this at the package level. Because this is where I've seen these most commonly applied, you could do these in a function level, if that made sense in your application. So I'm going to declare a constant a, and I'm going to have that as an untyped constant. And I'm going to set it equal to this special symbol called Iota. So when I run this, you see that a is evaluated to have the value zero, and it's inferred to
have the type integer. So what is Iota? Well, Iota is a counter that we can use when we're creating what are called enumerated constants. So in this example, having an enumerated constant isn't terribly valuable. But one of the things that I can do with constants is I can actually work with them in a constant block like this. So when I'm doing this, I can create another constant set that equal to Iota and another constant and set that equal to it, let's go ahead and clean up this because we already know what the type is going
to be. So we don't need to be printing that out. And then let's print this command out Two more times, switching to B, and C. So now we're using Iota three times and when we get the result we actually see Iota is changing its value as the constants are being evaluated. So the first constant that's assigned has the value of zero than one and then to know another special feature that we can take advantage of with Iota is that if we don't assign the value of a constant after the first one, then the Pilar is
going to try and infer the pattern of assignments. So in this example, we would expect to have an error because B and C don't have a value assigned. But since we've established a pattern for how to name the constants in this block, when we run, we actually get the same result. And that's because the compiler is going to apply the same formula. So it's going to apply b equals Iota and C equals Iota for us. Now that value of Iota is scoped to that constant block. So we create another constant block. And in this case,
we create a constant called a two and set that equal to Iota, copy this line, bring it down, and print out the value of A two, then what we're gonna find is Iota resets to zero. So Iota is scoped to a constant block. And what that lets you do is you can actually create related constants together, ensure that they have different values. And then if you have another set of related constants, you can start another constant block and ensure that they have unique values, but allow duplication between the values in one constant block in another.
So what's an example where you might use this? Well, let me just drop in this simple application. And what we're doing here is we're setting up a constant block, where maybe we're trying to store the specialty of veterinarians in a veterinarian clinic so that our narine could be a cat specialist or a dog specialist, or maybe we can take his neck specialist to. Now as you can see, inside the cost box, I'm setting the cat specialist equal to Iota. And then in the main block, I'm creating a variable and setting its value equal to cat
specialist. So if I check to see if the specialist type is a cat specialist, then I in fact, get the value true. Now, that works just fine. And this also works if I, for example, use a dog specialist, assign that specialist type to be a dog specialist run that, then that's going to work out just fine. So everything looks really good here, right? And this is a very common use for enumerated constants. However, one thing that I would warn you about is what happens if I declare this variable and don't initialize it to a type?
Well, if I check to see if it's dog specialist, I get false, which makes sense. But remember, what is the initial value of Iota? Well, the initial value of Iota equals the zero value for an integer. And so in fact, even though we haven't specified a specialist type, it does show up as the value can't specialist. So what do we do about this, so there's a couple of approaches that we can take here. The first is to use the zero value of the constant as an error value. So we can set this equal to error,
then we don't need this statement anymore. And now when we check to see if the specialist type is a cat specialist, We get the value false because cat specialists is equivalent to the integer value one, which is no longer the zero value of the integer. This is a very valuable approach, if you want to check to see if a value hasn't been assigned to a constant yet, so you can specify an error specialist. And then you can check to see if that value is set equal to the zero value of that constant. And if it
is, you can handle that error, because presumably, you expect that to be initialized in some way. Now, if your application guards against that, and there's no reasonable way for this to happen, then you can take advantage of this underscore symbol, which is goes one and only write only variable. Now what's the value of a write only variable? Well, with Iota, we have to have a zero value, we always have to start with zero. But if we don't care about zero, then we don't have any reason to assign the memory to it. So we can use
this underscore symbol. And we'll see this In quite a few places in our go applications. And basically, what that tells the compiler is yes, I know you're going to generate a value here, but I don't care what it is go ahead and throw that away. So if we run our application, again, everything works just fine. But in this case, we can't actually get at the zero value of this constant block. Now the ability to create it lists of enumerated constants with a Oda is very valuable. But things don't actually stop there. And the reason is,
remember, the value of a constant has to be able to be determined at compile time, but there are some operations that go is going to allow us to do for example, we can do addition. So if we do this and run, then we get false again. But what happens if we print out the value of kept specialist if we do that, and we're going to have to remove this line, then in fact, that expression got evaluated. So the first line line eight is evaluated to Iota plus five, which is zero plus five, the next line
cat specialist, Iota increments, and the formula repeats. So cat Specialist is equal to the value six, dog specialist is seven, and snake specialist is eight. So this can be valuable if you need some kind of a fixed offset. Now a common use case for this is to use the bit shifting operators because anything that we can apply to our primitive type, we can apply here as long as it's not a function expression. So we can do addition, subtraction, multiplication and division, we can do remainder operations, we can do the bitwise operations. And we can do
bit shifting, which is one of the more interesting use cases that we can take advantage of. And the reason is because we don't have the ability to raise two powers because raising two powers and go is a function in the math package. So we can't do that in our constant evaluations. But by bit shifting, we can raise things to the power of two, because every time you shift the number one level, you're actually multiplying it by two. So we have this example here. And I actually stole this from the effect of go article on golang.org.
So what we have here is we have an example Of a constant block that's giving you constants that are equivalent to kilobyte, megabyte, gigabyte, terabyte, petabyte, and so on. So down here, in our main program, what I've done is initialize the file size to some arbitrary value. And then I've got this printf statement. And this is basically going to format a result to print two decimal places, and then the literal string GB afterward. So this string here is basically saying I'm expecting to format a floating point number, and I'm going to give it two decimal
places, this GB is a literal GB, that's going to be printed in the result. And then we've got the value that's going to be used to fill this in. And that's going to be file size divided by the GB constant. Now you notice this constant block is set equal to one, and then we're going to bit shift that value 10 times Iota. So the first time we're going to bit shift 10 times one, so we're basically going to multiply this by two to the 10th. And then we're going to multiply by two to the hundreds
for the megabyte, and then two to the 1004, gigabyte, and so On. So when we run this, we get a really convenient way to format an arbitrary file size into a human readable format. And in the effective go article, it actually shows you how to put a switch block, which we haven't talked about. So you can make a decision about which constant you're going to use based on the size of the incoming value. So here, we get this nice way to format this relatively difficult number to read to be the very easily read 3.73 gigabytes.
Now another thing that can be very valuable to do is using bit shifting in order to set Boolean flags inside of a single byte. So if I paste this example in, we can see an example of that. So let's just say that we've got an application and that application has users and those users have certain roles. So inside of this constant block, here, I'm defining various roles that we can have. So for example, you might be an admin, you might be at the headquarters or out in the field somewhere, you might be able to see
the financials or see the monetary values. And then there may be some regional roles. So can you see Properties in Africa, can you see properties in Asia, Europe, North America, or South America. So in order to define these constants, what I'm doing is I'm setting the value to one bit shifted biota. So the first constant is admin is one bit shifted zero places, so it's a literal one, the second one is one bit shifted one place, that's two, and then four, and then eight, and then 16, and so on. So what I have is each
one of these constants is going to occupy one location in a byte. So down here in the main program, I'm defining the roles in a single byte. And I'm oaring together is admin can see financials and can see Europe now if you remember, oaring, is going to be set to true if one of the values is true, or the other one. So his admin has the binary representation of 0000001. I think that's enough zeros, seven zeros, followed by one can see financials is going to end up with 100, can see Europe is going to end
up with the value 100000. And so when we order those all together, we're going to get this Byte that has these three flags set to true. So when we run this, we see that we've encoded eight access roles for user into a single byte of data. So we're able to store this information extremely efficiently. So if I want to see for example, if this user is an admin, I can go ahead and print his admin, and then print out the value. And then in order to determine if that's valid or not, then I can do
a little bit of bitwise mathematics here. So I can take the constant is admin, and that with the roles, and what that's going to do is that's going to apply what's called a bit mask. So only the bits that are set in the is admin constant, and our roles are going to be left as true, which means if we're an admin, we're going to have the value one set at that first bit. And then I can compare that to the is admin constant. So when I run this, if we have the admin role, then we're
going to get the value true. Now, if I check something that I don't have the role, so let me go ahead and copy this down. And then let's just see if they're at the headquarters, so We'll put that in here. And it's exactly the same bitwise operations, we're just changing our mask. If we run this, we see that is headquarters equals false Actually, let me put in my Line return here, and then run this again. And you see that is headquarters equals false. So we can very quickly and very efficiently store a lot of different
information about what roles and access rights a user might have, and a simple byte. And having this constant defined with a numeration expression makes it really fast and really efficient and really clear in our application. Okay, so let's go into a summary and review what we've talked about in this video. constants are another one of those foundational elements, that is going to be a part of almost every application you're going to write. Now, the first thing that we learned about with constants is that they're immutable, but they can be shadowed. So we can create a
constant, we cannot assign a new value to it. But if we create a constant on an inner scope from an existing constant, then not only can we change the value, but We can even change the type, because that inner scope is going to shadow the outer scope constant, they have to be replaceable by the compiler at compile time, so the value must be calculable. So we're not gonna be able to access functions or command line arguments in order to determine the value of the constants in our application. But we are going to be able to
do simple expressions like we talked about in the enumeration section. They're named like variables. So if you want to export the value of the constant outside of your package, then you're going to use Pascal casing. And if you want to leave it as an internal value to the package, then you're going to use camel casing to name that constant. Type constants work just like immutable variables. So you can use them in arithmetic operations, you can pass them into functions, but they can only interoperate with the same type untyped constants have a little bit more flexibility.
So they work just like the literals. So if you replace that constant throughout your application with the literal value of that Constant, that's how it's going to work. So that's going to allow us to interoperate with similar types. So we had the value 42 defined as an untyped constant. And we could add that to an integer 16, we could add that to an integer, we can add that to a un 16. Any of those would work, because the literal 42 will work in all of those cases. Then we talk about the enumeration types that we
can work with. And we learned about the special symbol Iota. That allows us to start with as values zero and increments one time every time we use it inside the same const block. Now the one thing that we have to watch out for is that constant values that match the zero values of variables can cause subtle bugs in your application, because you might have logic that you expect it to initialize the value of the constant. And if something happens and then initialization doesn't occur, then you're going to be working with zero value, which might give
you a false match to a constant that you're evaluating Against. Using that Iota operator, we can actually create what are called enumeration expressions. So we can define the value of the constant dynamically by combining Iota with any arithmetic bitwise operation, or bit shifting operation that's allowable with the primitive type that the constant and representing, I want to talk about the first two collection types that we have available and go arrays and slices. Now, arrays form the basis of slices. So I want to start a discussion with those. And when we talk about arrays, we're going
to talk about how to create them the built in functions that go offers us to understand what's going on with our arrays. And then we'll do some exercises working with arrays and see how we can use those in our applications, then we're going to follow the same pattern, but we're going to switch over to slices. So we'll learn the various ways that we can create slices, we'll learn the built in functions that we can use to understand what's going on with our slices. And then we'll do some exercises working with those. Okay, so let's Go
ahead and get started by learning how to create an array. So the first thing that I want to discuss about arrays is the use case for them. Why do we need them and what are they used for? So let me just drop in an example, let's just say that we're building an application that's going to work with the grades of students in a class. So with that arrays, we're going to end up with an application something like this, we're going to have maybe grade one, grade two, grade three, and then we can print out those
grades. So we can go ahead and run this. And we see that we get the scores 9785 and 93 printed out. Now this works sort of. But we got a lot of problems here, because our application needs to know exactly how many grades we have to work with, at the time that we're designing the application. And working with these grades as a collection becomes very cumbersome. So enter the array, and that's going to solve all of our problems. So in order to see what an array looks like, let's go ahead and delete this code here.
And then we'll create an array, that's going To hold the grades for us. Now the way we declare an array is we're going to start with the size of the array. So we're going to use square brackets and then we're going to have an index that's going to be the number of elements that are array can hold, and then the type of data that the array is going to be designed to store. So an array can only store one type of data. So in this case, we're declaring an array of integers that can hold up
to three elements. If we wanted to hold a different type, say we want them to have an array of strings, then we would type this to a string, and so on. So you have to specify at the time that you're declaring the array, what type of data you're going to store. And then we can use this initializer syntax To put in the values for our array, so we can put in the same scores that we had before 9785, and 93. And then, if we come into our print statement here, and add our grades as what
we're going to print, then we see that we have all of the grades printed out together in this collection called an array. Right Now, that's a convenient collector. As we start getting into looping constructs and things like that, we're going to really find that having things grouped into arrays and slices and the other collection types is a very powerful way for us to work with our data. Now, another advantage that we have with working with arrays is the way that they're laid out in memory. So if you declare three different variables and specify their values,
it's impossible to know how they're going to be laid out by the go runtime with arrays. However, we know by the design of the language that these elements are contiguous in memory, which means accessing the various elements of the array is very, very fast. So by collecting our data together in arrays, not only is it easier to work with, but it also makes our applications generally a little bit faster. Now one problem that we have in this example here is if you look at it, we're actually declaring the size of the array twice, because we
have this syntax here, where we're saying that we're creating a three element integer Array, but then we're adding three elements to it. And that's not really required. If you're going to be initializing an array literal like we're doing here, then you can actually replace the size with these three dots here. Basically, what that says is create an array that's just large enough to hold the data that I'm going to pass to you in the literal syntax. So in this case, we're going to get an array that has three elements in it. And that's implied by
the fact that in this literal syntax, we've passed three integers to it, we can also declare an array that has a certain size, but has its values zeroed out, by doing something like this, if we declare an array called students, and let's make that a three element array that's going to hold strings. And then let's print out what we have in that array. So if we print out, students, make sure I'm spelling everything correctly and run this, then we see that we have an array that's empty. So we have declared a three element array that
can hold strings. But obviously, there's no elements in there right now. So In order to specify a value in the array, we're going to use this syntax, so we're going to call upon the array, and then we're going to tell it which index we want to work with within the array. So in this case, we're working with the zeroeth index of the students array. And then let's just assign the name Lisa to it, then we can go ahead and print out our array again, and run it. And now we see that we have, I always
forget to add this line return here, then we see initially we have an array of students that's full of empty strings. In the second instance, we've actually specified that first element. Now you may be wondering why we're starting with the value zero. And the reason is related to how arrays are made up of contiguous blocks of memory. So when we talk about students as the name of the array, what go is going to do is it's going to have a pointer or it's going to remember the location of the beginning of that array. And then
the index that we pass in this case zero, is going to tell it how many strings To walk forward. So it knows that when it has a string, a string has a certain length. And so it's going to walk that many strings. So when we pass zero, it's going to be the head of the students array moved forward zero string elements. And so that's going to be the first element of our array. So we can finish this example out, if I drop some code in here, we can see what it would take to fill out
this array. So in this case, we got the zeroeth element to Lisa, the first element is Ahmed. And the second element is Arnold. So if we print that out, we see the expected result, where we have Lisa, Ahmed and Arnold. And it doesn't matter what order we work with these, if we flip these around, then we find that they do flip around in the array, we can assign them in any order that we want. Now if we want to get add a specific element in the array, then we can use this square bracket syntax again,
and dereference the element from the array. So if we do this, and then change our label again. So we're going to get the second element of the array, Which is index one, then we can go ahead and run this. And then we see that the second element has the value of Arnold. So we can use this square bracket syntax in order to assign values to the array, as well as to pull out the values that have been assigned. Now another thing that we can do is we can determine how big the array is. Now, obviously,
we created the array up on line eight. So we remember at design time that we created this, but there may be a situation where you need to go back and review the size of the array that you're working with. And the way that we can do that is using the built in length function. So if I drop in another print statement here, and format that, you see that we can get the number of students in array using this built in alien function and passing in the array. So if we run this, then we see that
we get the number of students equals three and that's going to print out the size of the array. So if we change the size of the array to say five then the results of printing The array isn't going to change, But the size of the array does. Now one thing that's important to remember is that an array can be made up of any type, it just always has to be the same type for a given array. So we've been working with arrays of integers and arrays of strings. So we've been working with primitives. But this
example here shows that we can actually make up arrays have anything else. So in this case, we've got an array of arrays. So let's just say that we're working with some linear algebra. And we need the identity matrix, which is a concept that's used pretty often in linear algebra. So this array here stores a three by three identity matrix. So the first row is going to hold the values 100, the second row is going to hold 010. And the third row is going to hold 001. So if we go ahead and print this out, then
we see that we do in fact, get those values. Another way to look at this, and maybe a little bit easier to see is using this way here. So we're just going to declare the array of arrays, and then we're going to initialize each one of those rows individually. So this Reads a little bit cleaner, and might be a little bit easier for you to understand what's going on. And if we run this, we get the exact same result. Now the last thing that I want to talk about with arrays is something that's a little
bit different with arrays and go than in other languages. And that is that arrays are actually considered values. So in a lot of languages, when you create an array, it's actually pointing to the values in that array. So if you pass things around, you're actually passing around the same underlying data. But in go, that's not true. When you copy an array, as we're doing on line nine, here, you're actually creating a literal copy. So it's not pointing to the same underlying data is pointing to a different set of data, which means it's got to reassign
that entire length of the array. So if I run this, you'll see what I'm talking about here. So on line eight, I assigned an array on line nine, I created another variable b and assign that to a, and then on line 10, I changed the second element of the array to the value five. And What you see is that when I print out the array, it has the original values 123. But B has the new values of 153. So when you're working with these, you have to be a little careful, because copying arrays, especially when
we get into functions, if you're passing arrays into a function, go is going to copy that entire array over. So if you're dealing with a three element array, that's not a big deal, if you've got a million elements in your array that could slow your program down a little bit. So what do you do if you don't want to have this behavior? Well, we haven't talked about it yet. But I want to give you a hint right now in order to cover this completely. And that is this idea of pointers. So the way that our
program is working right now is that the value B is assigned to a copy of the array. But if we do the address of operation, which is this character here, then what we're saying is B is going to point to the same data that he has. Now we'll get into more detail about what this means later. But The long and the short of it is if I run this, now, A and B are pointing to the same data. So A is the array itself, and B is pointing to a. So when we change the value
in line 10, we're actually changing the same underlying data for both. So when we print them out, we see that the array has changed, as well as the array that B is pointing to, because they happen to be exactly the same array. Now arrays are very powerful. And there's a lot of use cases where you can use arrays very efficiently. However, the fact that they have a fixed size that has to be known at compile time definitely limits their usefulness. So in go, the most common use case for using arrays is to back something called
a slice. So let's take a look at a slice. So the first thing that I want to do is comment out a couple more items in this example here. So we'll comment on this one, this one and this one. And then we'll change this over to slice syntax. So the way we're going to do that is simply by eliminating these three dots here. So a slice is initialized as a literal by just Using the square brackets, the type of data we want to store. And then in the curly braces, we can pass in the initialized
data. So we go ahead and run this, we see that we get the values 123, it looks exactly like an array. And as a matter of fact, everything we can do with an array we can do with a slice as well, with one or two exceptions. So in order to illustrate that, we have the length function that we talked about with an array, well, we have the length function with a slice as well. So if I run this, we see that we do get the length of three. So we initialize the slice with values 123.
So with three elements, so the length function gives us a value of three. Now there's an additional function that we have available with slices, and that is the capacity. And that's because the number of elements in the slice doesn't necessarily match the size of the backing array, because the slice is really a projection of that underlying array. So we can have a very large array and only be looking at a small piece of it. Now if we run this example, we see that the capacity Function returns the value three. So the underlying array is exactly
the same size as the slice. But as we go along here, we'll see how we can get into situations where the length and capacity are different, and why that's a very good thing. Now unlike Like arrays, where we have this syntax here, and we have to use this address of operation in order to point to the same data slices are naturally what are called reference types. So they refer to the same underlying data. So if we run this example, again, remember when we ran this with an array, we saw that B store different data than
a when we were done. So if we run this, we see that A and B are actually pointing to the same underlying array. And so when we change the value in B, we get a change in the value in a. So this is one thing that you're going to have to keep in mind when you're working with slices. If you got multiple slices pointing to the same underlying data, you have to keep in Mind, if one of those slices changes the underlying data, it could have an impact somewhere else in your application. Now, so far,
we've only looked at the literal syntax for creating a slice. And that's what we're seeing here on line eight, there's actually several other ways that we can create slices. And that is illustrated with this example here. So you see on line eight, we're creating a slice that has the values one through 10. And then on line nine, we're creating a slice B, and that's using this bracket with the colon in between. And what that's going to do is, it's basically going to create a slice of all the elements of what it's referring to. So it's
going to create a slice of all of the elements of a online 10, we're creating a slight C, and that's going to start with a third element of the parent, and copy all the values after that. So that's going to start with the element with index three, which is, of course, the fourth element and every element after that, so this is going to copy four through 10, into the slice at sea, then on line 11, we're Going to do the other syntax, and that's going to copy everything up to the sixth element. And that's the
literal sixth element, that's not element number seven, that's actually the sixth element, which is going to have the index five. And then on line 12, we actually see an inner slice. So we're going to copy the fourth element through the sixth element into our new slice. So let's go ahead and run this and see what prints out. So we see the first line printed out is the original slice that we have. The second line is the copy of that slice that's copying all of the elements. The third line is going to copy the fourth element
on so we see the number four through 10. The fourth line copies everything up to the sixth element, so we get the values one through six. And then the last line printed out is a slice from three to six. And so we're gonna get the elements four, five, and six printed out. So that can be a little bit confusing, because the first number has a slightly different Meaning than the second number. So basically, the first number is inclusive, and the second number is XClusive. So for example, if we look at line 12, again, we're going
to copy from index three, up to but not including index six. So that's another way that you can look at it. Now one thing to keep in mind, remember what I said that all of these operations point at the same underlying data. So if we take element five, and change that value in the a slice and run this again, notice all of them teams value, because they're all pointing to the same underlying array. So each one of these operations includes the fifth index in their results. And each one of those gets updated to the value
of 42. Now another thing to know about these slicing operations is they can work with slices like we're doing here, but they can also work with arrays. So if you remember, if I put these three dots in here, it's actually going to turn a into an array. And if I run this, we get the same result. And that's because slicing operations can have as their source, an array or a slice. So whatever type of data you're working with, as long as it's one of those two, you can use these slicing operations. Now the last way
that we have available to us to create a slice is using what's called the make function. And that's a built in function that we have to work with. So if I delete all this, and drop this guy out, we're going to use the built in make function. And this takes two or three arguments. So let's start with two arguments. So the first thing we're going to say is the type of object that we want to create. So you can use make for several different operations. In this case, we're going to be talking about making slices.
So in this example, here, we're going to make a slice of integers. The second argument is going to be the length of the slice. So in this case, I want to start with three elements. So now let's just get some information about the slice that we created here. And we'll do that by dropping in a couple of print statements. So we'll print out the values of the slice, the length of the slice and the capacity of The slice. So they run this, no big surprise it zeroed out. So when I create a slice, everything gets
set to the zero value, which is what we always expect in go to happen every time we initialize a variable, we expect it to be initialized to zero values. And that's true for slices just like it's true for primitives. When we asked for the length, we get a length three, when we ask for the capacity, that's also set to three. Now we can also pass a third argument to the make function, and that's going to set the capacity. So keep in mind, the slice has an underlying array, and they don't have to be equivalent. So
if we run this, we see that we've created a slice of length three, it's got three elements in it, but the underlying array has 100 elements in it. So why would we do that? Well, the reason is because unlike arrays, slices don't have to have a fixed size over there and Life, we can actually add elements and remove elements from them. So in order to show you an example of that, let me drop in another example here. And this is going to start with a slice of Integers that starts with no elements in it. So
if we go ahead and run this, we see what we expect, we see an empty slice length of zero capacity of zero. Now, if I want to add an element to this slice, I can use the built in append function. So this takes two or more arguments, the first is going to be the source slice that we're going to be working with. So I'm going to start with a and I'm going to add an element to it. And in this case, all I want to do is add the number one to it. And then let's
go ahead and print out the value of the slice the length of the slice and the capacity of the slice again. So if I go ahead and run this, we see that in the second operation, we have a value one stored in there, we have the length of one, and notice the capacity is two. So that's kind of interesting. What happened here is when we initialize a slice to the value a go assigned a memory location for the slice. And since it didn't have to store anything, it basically created an underlying array of zero elements
for us. As soon as we added an element, obviously, It couldn't fit in a zero element array. So it had to assign an array for us. So what go does is it copies all of the existing elements, in this case, nothing to a new array that's got a larger size. So when we reassigned, it actually did create a new array, this one has a capacity of two, and then it put the value one into that array. Now, when we're dealing with small slices like this, things are pretty cheap and pretty easy, even if you're resizing
the array quite a few times. However, as things get very large, these copy operations become very expensive. And that's why we have that three parameter make function. That way, if we know the capacity is going to be somewhere around 100 elements, you can go ahead and start there. And that way, as you're appending elements and building up the slicer, you're not constantly copying the underlying array around. Now, if you remember, when I said the append function can take two or more arguments. The reason for that is this is what's called A variadic function. So everything
after the first argument is going to be interpreted as a value to append to the slice passed in the first argument. So if we have this example, here, we're actually going to append the values 234 and five to the slice returned by A. So if we run this guy, we see that we get the elements one through five created there, and the length is five, like we might expect. But now the capacity is eight knots not fixed in stone, how go resize of the arrays. But generally, what I've seen it does is once it fills
up the underlying array with a slice, when you add the next element, it's going to create a new array, and it's actually going to double the size from the previous array. So if we start with an empty slice, we see that the array would initially be of size zero, then we'll go to 248 1632, and 64 elements. So that's something else to be aware of, if you're just over one of those powers of two, you can actually end up with a lot of memory consumed that you're never going to be using. So again, if you
have the ability to come Up with a decent first estimate, then that's going to be beneficial to you. Now one common situation that you're going to run into is if you have a slice of elements, and another slice of elements, and you want to concatenate them together, so you want to have another slice created that has all of the elements of the first slice, and all of the elements of the second slice. So you might want to do something like this, if I convert this over to a literal slice, you might want to run a
command something like this. Well, this unfortunately, is not going to work. If I go ahead and run this, you'll see that we're going to get go complaining to us. And that's because the second argument to the append function has to have the type of the slice. So go can only accept integers here. It can't accept a slice of integers. But we do have a way around this. So I don't know what it's called an NGO in JavaScript, they would call this the spread operator, where if you have three dots after the slice, it's actually going
to spread that slice out into individual Arguments. So if we run this, this is going to work. And it works exactly the same as if this weren't a slice at all, it's basically going to take this slice and decompose it into looking something like this. So these work exactly the same way. But it's a convenient feature to know about if you have slices and you want to concatenate them together. Now, some other common operations that you might do with slices are stack operations. So let's just say that we're treating our slices a stack, and we
want to be able to push elements onto the stack and pop elements off of the stack. And things like that, with the append function is going to allow us to push elements onto the stack. But how do we pop elements off? Well, we have a couple of different ways that we want to do this. If we want to do what's called a shift operation, which means we want to remove the first element from the slice, then we can do this operation here. And what this is going to do is it's going to create a new
slice that starts at index one, which has the value two in this example, And takes everything else from that. So if we go ahead and print out the value of b, we'll see that that has the elements two through five. Now if you want to trim an element off of the end, then you're going to have to use a different syntax here. So we want all of the initial elements. So we're going to start with a colon, and then we'll use that length operation, figure out the length of the slice. But remember, that's going to
return the number that's too large, we actually want to remove an element off. So let's go ahead and do length minus one. And this will have the values one through four in it. So it's pretty easy to remove an element from the beginning of the slice or the end of the slice. But what happens if you want to remove an element from the middle? Well, here, things get a little bit hairy. Because what we have to do is we actually have to concatenate two slices together, the first slice is going to be all the elements
up to where we want to remove the element. So in this case, we can take the first two Elements, by doing a slice of a, and passing in colon two for this slice that we want to create, then we have to concatenate to that all of the element after the index we want to remove. So in this case, we're removing that middle element. So we'll take from three on and then we have to use this spread operation in order to spread things out so that the append function is happy. So let's go ahead and run
this and then get my syntax correct here. And now we see that we've got the elements 124, and five. So we've successfully removed an element from the slice, they have to be a little bit careful here. because keep in mind, we're working with references to the same underlying array. Because these are all slicing operations, there's one underlying array and everything is being done on that underlying array. So just to show you the havoc that we've created here, let me go ahead and print the value of a out here, and then print the value of a
out afterwards and go ahead and run that. And you see we start out with the elements 12345. Like we might expect, We do our slicing operation, removing the elements from the middle, we get 1245, which was what we might expect. But then when we print a out, things look a little weird, because we have that slice that V represents which is 1245, our three is completely gone, and five is duplicated. So this is something to be very sensitive about. If you're going to be removing elements from the inside of a slice using a command like
we have on line 10, make sure that you don't have any other references to that underlying array. Otherwise, you're going to get some unexpected behavior. So what do you do if you have a situation like this, and you really, really need a to stay the same, and you really, really need B to change? Well, unfortunately, we don't have the tools to work with this right now. Because what we're going to need to do is to use a loop to create a copy of that slice that doesn't point to the same underlying array, and then you
can make the manipulation. So when we get to the looping section, we'll show you how to do loops. And then you'll Have all the tools that you need in order to handle that situation. For now, the only thing I can say is be aware of this behavior. And if you get into a situation like this, understand that you're going to have to do a little bit of research in order to make sure that your application responds correctly. Okay, so that covers what I want to talk about with the raising of slices. Let's head into a
summary and review what we've talked about. arrays and slices are very common in NGO applications. And I think you're going to see them all over the place as you start to work more and more with the language. So I hope that this conversation has really helped you understand what arrays and slices are and some basics about how you can work with them. We started our discussion by talking about arrays. And we learned how there are collections of items with the same type. So you can't create an array that has strings and integers and things like
that all of the types have to be the same, but does collect them all together. And as we start to get into conversations about looping And things like that, we're going to find how we get some very powerful tools that we can work with, when we're basing our data on arrays and slices. They are fixed size. So when you create an array, you have to specify at compile time how big that array is. And it can never change. So you can't make them smaller, and you can't make them larger. There are three different ways that
we can declare an array, we can do the literal style, like you see on this first line, where we're going to specify the number of elements and then we use a literal that's going to initialize those elements. However, we do have a shorthand notation that uses three dots instead of that first size. And that's going to be a little bit more robust in your application design. Because if you run into a situation where you need to add another literal, you don't have to remember to update the size of the array, it's going to update automatically.
We also have this third syntax where we can declare a zero data array. In this case, we're going to declare an array of three integers, Each one of those integers is going to start with the value zero. We also talked about how arrays are zero based. So we're going to be counting from the head of the array. So the first element in the array has the index zero, the second element has the index one, and so on. So in this example, if we asked for the index one of array, we're going to get the value
three out, we have the Len function that's going to return the size of the array in case you need to know that some other place in your application. And you want to have a way to be able to handle that robustly. That's not relying on the fact that you know, at design time how big that array is, you can use the Le n function to interrogate the size of the array. Anytime you're moving the array around, it's going to copy the underlying data. So if you have a three element array, and you assign that to
another variable, all three elements are going to be copied to a new location in memory and you're going to be working with an independent copy. So that can cause some unexpected behaviors because If you change that copied array unexpected changes to be reflected back in the initial array, that won't happen. And it can be very expensive because all that memory has to be allocated a second time. Then we moved on to discuss slices and how they're very similar to arrays. As a matter of fact, they're backed by an array. So every slice that you see,
under the covers, go has an array that's holding all of that data for you. There are several creation styles, we saw that we can create a slice by slicing an existing array or another slice, we have the literal style that we can use, which is very similar to the array literal, except for we just leave those three dots out, because the size of the slice is dynamic. And so it can be determined at runtime, we also saw how we can use the make function. So if we pass two arguments to the make function, the first
is going to be the type of slice that we want to create. So in this example, we're going to create a slice of integers. And the Second parameter is going to determine the length and the initial capacity of the slice. If we need a capacity that's different than the initial length, then we can pass a third parameter into the make function. And that's going to allow us to specify a capacity independent of that initial length, the Len function returns the length of the slice itself, whereas the capacity function returns the length of the underlying array.
So if for some example, you want to control the resizing of the slice, you can go ahead and do that. And use that capacity to function to understand when you're starting to get to your limits, you can also use the append function to add an element to the slice. And what that's going to do is it's going to take in a parent slice, and it's going to take in one or more elements to add to that slice. Now through the course of that append operation, you can add elements that exceeds the capacity of the underlying
array. If that happens, then you are going to trigger a copy operation. And all of those elements are going to have to be copied to A new location. So be aware of that. If you're dealing with large slices of data, and you end up resizing, a lot of times your application performance can suffer, you might want to think about using that three parameter make function, in order to set the capacity of the slice close to where you need it to be. When you're passing slices around in your application. Keep in mind that assigning a new
value to an existing slice actually points to the same underlying array. So if you manipulate the slice in one location, it's actually going to affect all the other slices that are pointing to that same underlying data. So we talked about an example where we created a slice and removed the middle element out of it. And we saw how that actually affected the initial slice, and could cause some behavior that we weren't expecting. I want to complete the discussion, I started in the last video by talking about the two remaining collection types that are available and
go. And those types are mapped in structs. So we'll start our discussion by talking about maps, we'll Talk about what they are, how we can create them, and then how we can manipulate the data within the maps, then we'll move on to the struct data type, we'll talk about what they are, how we can create them, then we'll move on to the naming conventions that we're going to have to be aware of as we're working with structs, then we'll talk about a concept called embedding, and how we can use that to have a struct that
we're creating inherit a lot of functionality from a base struct, then we'll finish our discussion of structs by talking about a concept called tags, and how we can use tags to provide some additional information about the fields within our structs to help the consumers of objects created from the struct get additional information about how that field is intended to be used. Okay, so let's begin our discussion by talking about what maps are and how we can use them. So the first thing that we're going to need to get our heads around when we're talking about
maps is what exactly a map is. And I think the easiest way to show you what a map is, Is by showing you a map. So we have an example here of a variable called state populations. And this represents a map of US state names over to the population within those states. So what we see here is a map is going to take some kind of a key, in this case, the state names, and it's going to map that over to some kind of a value, in this case, the population of that state. So what
this provides us is a very flexible data type. When we're trying to map one key type over to one value type. Now there's a couple of constraints we're going to have to keep in mind. As you can see up in the Declaration on line eight, we're going to map one key type to one value type. So all of the keys in this map have to be of type string, and all of the values have to be of type integer. Now we can use a lot of different types for the keys. And we can use any
type for the value. But when we declare a map, that type has to be consistent for every key value pair within the map. So if we run this, we can see an example of what A map looks like when we print it. And it's not the best output in the world, but we can get an idea of what's going on. So we see that we get the key value pairs printed out. So we get California, Texas, Florida, New York, Pennsylvania, Illinois, and Ohio. So one thing that I just alluded to is that we have a
lot of options with the key type, but we don't have an infinite number of options. So the basic constraint on the keys when you're creating a map is they have to be able to be tested for equality. Now most of the types that we're working with can do that. So Boolean can be tested for equality, all of the numeric types, strings, pointers, interfaces, structs arrays, and this thing we haven't talked about called channels. All of those can be tested to see if one instance of for example For a string variable is equivalent to another one.
However, there are some data types that cannot be used for equivalency checking. And those are slices, maps and other functions. So for example, if we create a map called m, and we'll make that a map of, say we want To map slices of integers over two strings. And we'll use the literal syntax for that, and then we print that out, then you would expect a map to print out. But in fact, we get an error because we've got an invalid key type because a slice cannot be a key to a map. However, if we had
an index here, then it turns it into an array. And now we do get a successful printout, of course, the map is empty. So we don't see anything, we just see this empty map right here. But we were successfully able to create the map, because an array is a valid key type, but a slice is not. So what are the different ways that we can create a map? Well, you see, the first way here, this is the literal syntax, and this is going to probably be the most common way you're going to see maps created.
So we just need to declare the type of map. And we're going to do that using this syntax that we see here on line eight, where we're going to start with the map keyword in square brackets, we're going to list the type of the key, and then after the square brackets, we're going To list the type of the value. Another way that we can do this is we can use the built in make function again. Now we first saw the make function when we were talking about slices. But we can use that once again here.
So if I copy this variable up here, and I set this equal to the result of calling the make function, and we're going to tell it what type of map we want to make. So we're going to use the map keyword again, the key is type string, the value is type integer. And now we can go ahead and remove this colon. And if we run this, we have to remove our m again from our print statement. Now if we run it, we get the same value printed out. So this is a common way that you
can work with maps, if you don't have at the time that you're declaring the variable, the entries that you're going to want to put in it. So for example, if you're populating the map within a loop, you might use this syntax. Now another option that you have with the make is it will take a second parameter. So if I run that like this, then that works. However, I wasn't able To find the intention for this. So this is available. But it doesn't seem to affect the length of the map that's created, the map is always
going to be the length of the number of elements in it, but it might have some effect under the covers. If you find that what that's for, please leave a comment down below. So that I can learn along with you know, let me go ahead and remove this because I don't know what it's there for. And I haven't seen that very commonly used and run this again. And we can see that we're back to where our map used to be. Now how are we going to manipulate the values in our map? Well, the first thing
that we can do is we can pull out one single value from the map by using the square brackets and typing in the value of the key. So the key can be provided either as a variable or as I'm doing here as a literal. So if I go ahead and run this, we see that we get the population of Ohio is 11 point 6 million people. So we can interrogate the values of our map. Using this syntax here. We can also add a value to our map using a Very similar syntax. So we call on
the state populations variable. And I add a key. In this case, let me add the next largest state, which is Georgia. And it as of 2016, had a population of 10,310,371. Now I can pull that value back out, run this. And we see that we get that value printed out of our map. If we print the entire map out, we see right here, Georgia gets added in here. Now, something you might have noticed, let me do this, I'm going to copy this print line here, put it above where we added the Georgia key. And notice
the ordering is different. And this is going to be a very important thing to keep in mind as you're iterating through maps later on, the return order of a map is not guaranteed. So even though we declared California, Texas, Florida, New York, Pennsylvania, Illinois, Ohio, and then added Georgia, this is not like a slice, a slice or an array would return those elements in exactly the same order we provided them. In a map, everything is stored in a way that cannot guarantee the order upon Returning. So even though we just added one entry here, it
completely changed how everything was organized within the map, and we get it a little bit different output. Now another thing that I want to show you is that we can delete entries from the map. So we can do that using the built in delete function. The first is the map that we want to delete from, and then we need to provide the key that we want to delete. So if we go ahead and delete Georgia back out, it wasn't there very long. But if we run it looks like I need to add my s here.
Now if we run it, we see that ga is no longer part of our map. So we can add by just calling on the map providing a new key and value. We can delete using the built in delete function, and we can interrogate a value from the map using those square brackets. Now, an interesting thing about deleting is if I asked for the value of that key again, what do you think I'm going to get? So we see here in the output, Georgia is no longer part of the map. So you might expect some sort
of error to be raised. Well, in fact, If I run this We get the value zero out. Now that should cause you some concern, because does Georgia have a population of zero? Or is it missing from our map? Did I just misspell the key? What's going on? So for example, if I fat finger, Ohio, and I forget the I and run this, I get a value zero. Well, did everybody move out of Ohio? What happened? What with what we know right now, there's no way for us to really now. So there's another way that we
can interrogate our map. And that is using what's called the comma. Okay, syntax. So with doing what we know, right now, we're basically doing this, we're calling state populations. And we're asking for the key, Ohio, and I'll continue that misspelling. And then we're printing out the value of that population variable, and we get the value zero. Well, we can also add an optional comma, okay, here. And when we do this, let's go ahead and add that okay, variable to the output and see what that's going to do. So notice that this prints out the value
false. So the okay is false if the key was not found within Our map, but if we add the eye back in and correct our spelling, we see that we get the value true out. So if you're in a situation where you're not sure if the key is in the map or not, then you can use this comma, okay, syntax. As a matter of fact, if you just want to check for presence, then we can use that right on the operator again, in order to throw away the actual value. And then we just get the
okay variable printed back out. Now, there's nothing magic about the variable name, okay. But it is conventional to use, okay, and you go programs when you're using it for this kind of a test. The next thing that I want to show you about maps is we can in fact, find out how many elements are in them. So we can do that using the built in Le n function. So if I go ahead and run that, we see that we get the value seven, and we have let's see 12345 yet we have got seven states declared
in our map right now. So if we added another state back in, then the length function would return eight. The other thing that's interesting to know is that just like slices, when you Have multiple assignments to a map, which is especially important when you're passing maps into functions, the underlying data is passed by reference, which means manipulating one variable that points to a map is going to have an impact on the other one, so I can demonstrate that by creating another variable called SP, and set that equal to state populations. And then let's go ahead
and delete poor Ohio out of SP, it always seems to get left out. And then we can print out SP and print out state populations. So if I run this, we see that in our first result, we don't have the entry Ohio anymore. And then our second print statement, which is the original map, Ohio has been removed from there as well. So if you start passing maps around and you start manipulating the data within that map, keep in mind, you can have side effects on the calling functions or any other place where that map is
referred to. Because manipulating the map in one place is going to have impacts on every other place to use. So the final collection type that I want to talk about Today is called a struct. Now at first, you might not think of a struct as a collection type. But go with me on this, if I drop in an example, we see here an example of a struct. Now I'm in kind of a doctor who frame of mind today. So we're going to run with an example that's talking about some information about one of the doctors
from Doctor Who. So if I go ahead and run this, we can see that we're getting information on the third doctor, the actor's name is john Pertwee, we see some of his companions listed out here. So the reason I call this a collection type is look at the declaration or the struck on line seven through 11. What we have here is a list of field names. So we've got number, actor name and companions as field names, and then a type associated with that field. So the doctor number is an integer, the actor name is a
string, and the companions is a slice of strings. So what the struct type does is it gathers information together that are related to one concept, in this case, a doctor. And it does it in a very flexible way. Because we don't Have to have any constraints on the types of data that's contained within our struct, we can mix any type of data together. And that is the true power of a struct. Because all of the other collection types we've talked about have had to have consistent types. So arrays always have to store the same type
of data slices have the same constraint. And we just talked about maps and how their keys always have to have the same type. And their values always have to have the same type within the same map. When a struct we can have our fields describe any type of data that we want. So we can have structs that contain other structs, anything that we want. So while in a lot of ways, structs are the simplest type of collection that we're going to be talking about. They're also one of the most powerful. So we see down here
in the main function, how we can create a struct and I'm using the declaration syntax, where I'm using named fields. So we're going to create a variable called a doctor. And we're going to use this literal syntax. So we list the type of the Struct doctor, we use the curly braces like we do with every other literal definition. And then I have a list of the field names colon and the value. So number colon three after name colon, john Pertwee, and then campaign To set equal to this slice of strings now notice that when I'm
setting something equal to a slice, I do actually have to tell go, what kind of collection type, I'm initializing for that, and then down here on line 23, I go ahead and print the entire value of the struct out. And you see that I get the values printed out, I see three, john Pertwee, and then the three companions that john Pertwee had, in his time as the doctor. Now if we want to interrogate one value from a struct, then we're going to use what's called the dot syntax. So if we ask for the actor name
of this struct, then we're going to put a dot after the variable name, put in the field name. And when we run this, we see that we get john Pertwee out. So we can work with the struct as a whole, or we can start drilling down. As a matter of fact, we can drill down Through the structure. So if we ask for the companions, we can get that out, certainly. But this is a slice like any other slice. So if I just want the second item in the collection, I can get Joe grant out by
interrogating the slice that gets returned by asking for the companions field. Now another way that we can instantiate our struct is using what's called a positional syntax. So here, I'm listing the field names, but it don't have to. So if I go ahead and take these out, and take this out, so I print the entire struct out, then I get exactly the same result as I got the first time. Now, this is valid go syntax, I would encourage you not to use it, though. And the reason for that is it can become a maintenance problem.
So for example, let's just say that this is our struct, and this is checked into source control, and everybody's happy with it. But then let's say we have a change request Comm. And we're going to add a list of the episodes that each doctor appeared in. So that's going to be another slice of strings. And that gets added right Here. Well, if we use the positional syntax and try and run this, we've got a problem. Because go doesn't know how to map the fields that we provided into the initializer. Because there's three values provided in
the initializer. And there's four values in the struct. So we have to find every place that we have one of these declared with the positional syntax. And we have to add, for example, a placeholder or populated or something. So we go ahead and run this and everything's working again. But now, what happens if another change comes along, and somebody does this? Now they've changed the order of the fields. When we run this, everything looks fine. But when I asked for the doctor's companions, again, I get an empty slice, because the positional syntax requires the fields
to be declared in the correct order, much better for us to go ahead and add in the field names. So let me drop those in real fast here. Now when I run this, I get the expected results out. And notice What the field names syntax, I don't even have to have it in the same order as they're declared in the struct go is going to figure out how to map the data from the initializer over into the object that's created by using those field names. The other advantage that I have is, if I don't have
any information about the episodes at this point in my program, I actually can ignore the fact that that field exists. And what this means is I changed the underlying struct without changing the usage at all, which makes my application a little bit more robust and change proof. So while it is possible for you to use the positional syntax, I would strongly recommend you do not use it unless you've got a very short lived struct. We'll talk about anonymous structs here in a second. And in those situations, positional declaration might make sense because that struct is
not going to live for very long. However, I would strongly recommend anytime you're taking the time to declare a type, like we're doing here on line seven, then you're generally going to be better served By using field names explicitly instead of the positional syntax. Okay, the next thing I want to talk about are naming conventions. So as you can see, here, I've got my type declared as Doctor with a capital D, and the fields declared as lowercase. So the rules in this situation, follow the rules for any other variable and go, if we start with
a capital letter, it's going to be exported from the package. If we start with a lowercase letter, it's going to be internal to the package. So in this case, I can work with all of the fields, because I'm declaring the doctor type in my main package. And so my main function, which is also in the main package has access to it. However, nothing in any other package would have access to this. So it would be able to see that there is a doctor struct, but it wouldn't see any field names. So if I did want
to publish these out, so anything can use them, I would have to go ahead and capitalize these field names, of course, or have to capitalize them in my declaration as well. And then when I run I get the exact Same result that I got before. So I forgot to change this variable as well go ahead and run that, and I get the expected result out. So generally, we're going to follow the same convention as we do with any other variable. uppercase is going to export lowercase is going to import, you should use Pascal casing and
camel casing, you shouldn't have underscores and things like that in your field names or in your struct names. Now if you notice here on line seven, I have explicitly created a type called a struct. And that's going to be a very common way that you're going to see struct to use. You see you're going to define a type and then everywhere you need to use that struct, you can just refer to it by its type. But we don't have to do that. And there are some situations where you might see a program like this. So
on line eight, we're declaring what's called an anonymous struct. So instead of setting up a type, and saying, doctor, and that's going to be a struct, and that's going to have a single field called name, that's going to take a string. We're Condensing all of that into this single declaration right here. Now, I can't use this anywhere else, because it's anonymous. And so it doesn't have an independent name that I can refer to it. But I can certainly use this just fine. So let me go ahead and delete this. And then notice I'm using the
initializer syntax right here. So we got two sets of curly braces, it's very important to remember what each one is doing. So the first set of curly braces is paired to the struct keyword. And it's defining the structure of the struct. The second is the initializer. And it's what's going to provide data into the struct. So if we run this application, we see that we do get the struct with the value john Pertwee printed out and everything's gonna work just fine. Now, when are you going to use this, this is going to be used in
relatively few situations, basically, you're going to use this in situations where you need to structure some data in a way that you don't have in a formal type. But it's normally only going to be very short lived. So you can think about If you have a data model that's coming back in a web application, and you need to send a projection or a subset of that data down to the client, you could create an anonymous struct in order to organize that information. So you don't have to create a formal type that's going to be available
throughout your package for something that might be used only one time. Now, unlike maps, structs are value types. So if I take this example here, and let's just use the same basic manipulation that we've been doing with all of these collections, so I'm going to create another struct, and I'm going to assign it to the value of my current struct, and then I'm going to manipulate the value of that name field. So I'm going to create another doctor called Tom Baker. And then let's see what happens when we print both of these doctors out. So
as you can see, even though I copied another doctor from a doctor, and change the value, the values remain independent. So a doctor still has the name John Pertwee, another doctor has the name Tom Baker. So unlike maps, and slices, these are referring to independent datasets. So when you pass a struct around in your application, you're actually passing copies of the same data around. So if you've got structs, that are very, very large, keep in mind, you're creating a copy every time you're manipulating this. Now, just like with arrays, if we do want to point
to the same underlying data, we can use that address of operator. And when we run this, we have in fact, both variables pointing to the same underlying data. A doctor is the struct itself. Another doctor is a pointer to the struct. So when we manipulate its name field, we're actually manipulating the Name field of the a doctor struct. The next thing that I want to talk to you about in go is a concept called embedding. Now, you may have heard in your research on the go language that go doesn't support traditional object oriented principles. So
for example, we don't have inheritance, and oh, my goodness, how am I going to create my program if I don't Have inheritance available? Well, let me show you what go has, instead of an inheritance model. It uses a model that's similar to inheritance called composition. So where inheritance is trying to establish the is a relationship. So if we take this example here, if we were in a traditional object oriented language, we wouldn't want to say that a bird is an animal, and therefore a bird has a name a bird has an origin, a bird has
also bird things like its speed, and if it can fly or not, go doesn't support that model. But instead, it supports composition through what's called embedding. So right now we see that animal and bird are definitely independent structs, there's no relationship between them. However, I can say that a bird has animal like characteristics by embedding an animal struct in here like this. So I've just embedded the struct itself, I haven't provided a name. Now, if I did something like this, then how to have a named field in the bird called animal. But I'm not doing
that I'm doing this. So I'm just saying embed the animal struct right into the bird struct. Now how can I use this? Well, let me drop in a new main function here. And then you can see, so I'm creating an instance of a bird. And then I'm initializing the variables. So name is going to be E mu origin is going to be Australia, the speed is 48 kilometers an hour, and it cannot fly. So if I print this out, I see that other than a little bit of a strange syntax with that inner type, I
see that I have all of my fields defined, it's a matter of fact, I can come in here and dig into any one of those animal fields. And everything worked exactly like I would expect them to so it kind of looks like bird is inheriting the properties of animal. But really what's happening here is there's some syntactic sugar go is handling the delegation of the request for the Name field automatically to the embedded animal type for you. So if you try and pass these around and look to see if the bird is a type of
animal, it is not bird is still an issue. Independent struct that has no relationship to an animal other than the fact that it embeds it. So it's not A traditional inheritance relationship where a bird is an animal. Instead, it's a composition relationship, which is answering the question has a, so a bird has an animal, or has animal like characteristics is how we would say it in this example. But it is not the same thing as an animal, they cannot be used interchangeably. In order to use data interchangeably, we're going to have to use something called
interfaces, which we'll talk about a little bit later. Now we can see in this example, that if we declare the type, and we initialize the variables afterward, everything's pretty clear, we can just treat all of the fields as if they're owned by the bird struct. And we don't have to worry about the internal structure of the struct. And that's very intentional. However, if we're going to use the literal syntax, we do have to know a little bit about the birds internal structure. So let me drop in this code here, clean up our formatting a little
bit, and then remove these lines here. So this is declaring exactly the same object, we're going to have an E mu from Australia, The speed is actually going to be 48 kilometers an hour, and can fly is going to be false. But notice what I have to do here, I have to explicitly talk about the internal animal struct. So when I'm working with the literal syntax, I have to be aware that I'm using embedding. But if I just declare the object and manipulate it from the outside, I don't have to be aware of it at
all. Now, when should you use embedding? Well, I will say Generally, if you're talking about modeling behavior, embedding is not the right choice for you to use. When we get into methods, we will see that embedding will allow behaviors or methods to carry through into the type that has the embedding. However, the fact that we can't use them interchangeably, is a very severe limitation. Generally, it's much better to use interfaces when you want to describe common behavior. So when is embedding a good idea? Well, if you're offering a library, for example, and let's just say
you're making a web framework, and you've got a very sophisticated base controller, in that case, you might want To use consumers of your library to embed that base controller into their custom controllers, so that they can get useful functionality out of it. In that case, you're not talking about polymorphism. And the ability to interchangeably use objects, you're just trying to get some base behavior into a custom type. And in that case, embedding makes sense. The last thing that I want to talk about with structs is a concept called tags. So let me just drop in
another example program here that we can work with. This is a simpler version of what we had before, because we don't need to structs for this conversation. So we're just going to have a simple animal struct with the name of the origin fields. And then what I want to do is I want to add what's called a tag in order to describe some specific information about this name field. So let's say for example, that I'm working with some validation framework. So let's just say that I'm working within a web application, and the user is filling
out a form and two of the fields are providing the name and the origin. And I want to make Sure that the name is required and doesn't exceed a maximum length. So I can do that with a tag that looks something like this. So the format of a tag is to have backticks as the delimiters of the tag, and then we have these space delimited key value pairs. So my first field is going to be required. So I'm going to say that the Name field is required. And then I've got a max length argument, which
in this case, is set to 100 characters. So the value of the tag is arbitrary, you could actually put any string that you want in here, but this is the conventional use within go, we're going to use space delimited sub tags. And then if you do need a key value relationship, then you're going to use a colon to separate the key and the value. And then the value is typically put in quotation marks. So now that we have this, how do we get at it? Well, the way that we're going to get at it is
using gos reflection package. So there's no straightforward way from an object to get at the tags of a field, you have to use reflection in order to get that, unfortunately, Go makes this pretty easy. So the first thing that I need to do is I need to get the type of an object that I'm working with. So I get that using the reflect packages type of function. And I have to pass in an object to this. So I will just initialize an empty animal to get them that and then I can grab a field from
that type, using the types field by name method, and then passing in the name. In this case, I want the name name. And now I can get at the tag by asking for the tag property of the field. So if I run this, we see that I do in fact, get that tag out. Now, what do I do with that? Well, that's really up to my validation framework to figure out all tags do is provide a string of text and something else has to figure out what to do with that. So this tags value is
meaningless to go itself, we're then going to have to have some sort of a validation library that's going to have to parse this figure out that yes, required is part of the tag. And then it's going to have to decide well, if required is there and then I'm going to apply this logic making Sure the string is non empty or whatever makes sense in our use case. Okay, so that finishes up what I have to talk about with maps and structs. Let's go into a summary and review what we've talked about. In this video we
talked about the two remaining collections types that are available and go, we talked about maps, and we talked about structs. And when we talked about maps, we learned that they are collections of value types that can be accessed by keys, they can be created as literals, or via the make function, and members are accessed via the square bracket key syntax, we can check for the presence of an element within a map using this comma, okay syntax. So we can ask for the value at a comma, okay, and what the map is going to return is
the value type as well as a Boolean, that's going to indicate if the value was found or not. Now, if the value wasn't found, your value variable is going to be the zero value of the value type in that map. So for example, if you've got a map of strings to strings, and the element isn't found, you're going To get an empty string returned out, if you've got a map of string to integers, for example, you're going to get the value zero. If you have multiple assignments to the same map, they all point to the
same underlying data. So they're what are called reference types, which means that if you manipulate the data in a map in one location, any other variables that are pointing to that same map are going to see that change as well. We then went on to talk about the struct type, and how they're a collection of disparate data types. So whereas arrays and slices and maps are all collecting the same data type together, a struct is unique in that it collects any type of data together. So you've got a common field name. But those fields can
point to any valid data structure within go. They're keyed by name fields, which generally follow the syntax of any valid variable, including whether they export it or not. So if you capitalize the name of the field, then that's going to be exported from the package. If you leave it lowercase, then that's going to be kept internal to the package. They're Normally created as types. But we can create anonymous structs if we need to. And again, common use cases for this are very short lived structs, such as generating a JSON response to a web service call,
that's only used one time. structs are value types. So if I assign a variable to an existing struct, then all of the struct values are going to be copied over to create a brand new struct. So if I manipulate a struct in one location, it's not going to affect any other variables in your application. We don't have an inheritance system within go. But we can use composition using what's called embedding. So when we want to embed one struct within another, we just list the type of the struct, and we don't give it a field name.
Go is then going to automatically interpret that for us and delegate any calls for fields or methods in the containing struct down to the embedded struct. If that top level struct doesn't contain a member with that name, we also learned a little bit about tags and how they can be added to struct fields to describe That field in some way. So there's a lot of different use cases. For this. The JSON package uses this to provide ways to map field names from NGO field names, which are typically uppercase to follow JSON conventions, which are typically
lowercase. We also saw that we can use this, for example, to pass information into a validation framework. And that framework could generate errors. If we need fields to be required, or have rules about their length or things like that, I want to start a discussion about the tools that go makes available to control the flow of execution in our applications. In today's video, we're going to focus on two of these tools, the if statement and the switch statement. We'll start a discussion by talking about if statements. And we'll talk about all of the different ways
that we can use them in our applications. And we're going to specifically focus on the various operators that are often associated with working with if statements. And we'll go through what those are and how you can use them. And then we'll talk about the relatives of the if statement. And those are the if else statement. And the if else if statement. We'll then move on to talk about switch statements. And we'll start that conversation with some simple use cases for switch statements. We'll then talk about cases with multiple tests, how we can follow through from
one case to another in our switch statements. And then we'll talk about this special version of a switch called a type switch. So let's go ahead and jump in and start learning about if statements. In order to start our discussion about if statements, I want to start as simply as we can. So if you look at this application here, it's got just about the simplest if statement that I could come up with. So we're going to start with the if keyword as you see here on line eight. And then we're going to have an expression
that generates some kind of a Boolean result. And that's actually where we're gonna spend a lot of time when we're talking about if statements is how to generate these Boolean results. In this case, I'm just using the literal true, which is the simplest Boolean Result that we can come up with. And then inside of these curly braces here, I've got the code that's going to execute if that Boolean test returns true. So in this case, since we're using a literal true, then the test is always going to pass. And when I run this application, I
see that I get this test is true printed out in the results here, if I change this to a literal false, and we see that we don't get any output from the application because the code inside of the curly braces is not executed. Now, one thing to keep in mind if you're coming to go from another language, is you may be expecting this to be valid syntax. Well, if we try and run this, we actually are going to get a syntax error because one of the design decisions that was made with go is that You're
never allowed to have a single line block evaluated as the result of an if statement. So you always have to use curly braces, even if you only have one line to execute. Now, this style of this statement has the Boolean test right here. And that's the only thing after the if clause. There's Actually another style that's very commonly used in the go language. And that's what I would call the initializer syntax. So as you see here on line 17, we've got a call into a map that's pulling out the value from a map, and it's
pulling out the okay variable. And then notice I've got this semi colon, and then the Boolean result listed here. So this part here, this first part of the if statement, is the initializer. So the initializer allows us to run a statement and generate some information that's going to set us up to work within the F block. So in this case, I'm generating a Boolean result by interrogating the map. And then I'm using that as the test to see if the code inside the if block should execute or not, I also have access to the pop
variable that I'm creating right here inside of the F test, and that variable is going to be scoped to the block. So if I execute this, you see that I get the population of Florida printed out, no problem there. If I try and work with that variable outside of here, and printed out again, I'm going To get an undefined error, because the pop of variables is only defined within the scope of that if statement. The next thing that I want to talk about with if statements are the comparison operators that we have. I mean, obviously,
as long as we can generate pure Boolean values, things are going to be pretty simple. But that's a pretty limited benefit, because in a lot of applications, we need to do some kind of a comparison in order to determine if the branch should execute or not. So let me drop in this example here. And let's just say that we're building some kind of a number guessing game. Now to keep things as simple as possible. This is going to be a pretty simple number guessing game, because we're going to hard code the value that we're going
to guess. And we're going to hard code our guests. So we're not going to have a user interface involved here at all. And we're not going to randomly generate the number that way, it's very clear what the application is doing. So right now what we have is we've got the number that we're Going to try and guess we've got our guests here on line nine. And then we've got a couple of tests. So in line 10, we're checking to see if the guess is less than the numbers. So this is the first comparison operator that
we're looking at. This is the less than operator, we also have the greater than operator and the equality operator. So if we run this with the guests of 30, and a number of 50, we're going to get the value to low printed out because this first test evaluates to true. And the second and third test evaluate to false. If I change my guest to 70, and run, then the second test evaluates to true and the first and third evaluate to false. And as you might expect, if I put in a guess of 50, and run
this, then I finally got it. So this is the basics of a number guessing game, all you need to do is wrap this in loops, which we'll talk about in a future video and add a little bit of a user interface and you've got your first game written in the go language. Now there's some other operators that we have available. And it's a little hard to justify Getting them into this example. So I'm just going to go ahead and dump them here. And then we can take a look at them. So this first test is
called the less than or equal operator. So it's going to check to see if number is less than or equal to guess the second is the greater than or equal to operator. And the third is the not equal operator. So if I go ahead and run this, you see that we are less than or equal to because we're exactly on the number we are also greater than or equal to, and we fail the non equal t test. So we get true true false printed here, if we go with 30. Again, we're going to get false
true true. And if we go with 70, we're going to get true false true. So these are the six different comparison operators that you're typically going to use in your applications. And these work with all numeric types. They don't however, work with things like string types. So if you're going to work with string types, typically what you're going to work with is the equality operator or the non equality operator. And that goes for any kind of reference Type as well. Now the next thing that I want to do in this example is add in some
kind of simple validation. So if you imagine that we've got some kind of a simple user interface where the user is asked to enter a number on the keyboard, and then we're going to use that as our guests, then we're going to need some way to validate that number to make sure that for example, they don't enter negative five. So to do that, we can add another logical test. But we can also combine multiple tests together using what are called logical operators. So let me drop this code in here. And you can see our first
logical operator. So this code here is conducting two tests, it's checking to see if the guest is less than one. Or if the guest is greater than 100. And if it is, then we're going to print out a message here, we can also put a guard around our actual game code, checking to see if the guess is greater than or equal to one. And this is the AND operator less than or equal to 100. So this first test case is going to execute if the guess is out of range. So if we enter Negative five,
for example, then we're going to run this and we see that the guests must be within one and 100. If we enter a number that's within range, then everything executes like we expect it to and this code on line 11,000 execute. So this first operator here is called the OR operator. And this is checking to see if the test on the left is true, or the test on the right is true. So obviously, we can't have the guest less than one and greater than 100 at the same time, but one or the other could be
true. And if one of those is true, then we're going to have an invalid guess. And it makes sense to print out a message to the user saying that this other operator that we see here is called the AND operator. And it evaluates to true if both the test on the left and the test on the right evaluate to true. So if we had a guest, for example of 105, then this code is gonna evaluate to true because it is greater than or equal to one. But this code is not because 105 is not less
than or equal to 100. So with an and test, both cases have to be True, unlike an or test, we're only one of the two has to be true. So a guess of 105 is going to print out our error message. And it is not going to execute the code within this if statement here. The other logical operator that we have is called the NOT operator. And what that's going to do is it's going to take a Boolean, and it's going to flip it to the other side. So if I take and just as a
simple example, if I take and print the value true out, then we're going to get the value true printed out here at the end of our program. If I put the NOT operator, which is just an exclamation point, and run that, then the true becomes false. Similarly, if I put this in front of a false, the false becomes true. And I can just prove that false is really false by taking that away, and false is false. So those are the three logical operators that we have. We've got the OR operator, which is the double pipe
that you see here on line 10. We've got the and operation which is the double ampersand, which you see here on line 13. And then we have the knot operation, which reverses a Boolean result. And that's done simply using the exclamation point. The other interesting thing to know about working with logical operators, is a concept called short circuiting. So in order to show you that, I'm actually going to have to jump ahead and add a custom function here. And we haven't talked about this. So let me just give you a simple of an explanation as
possible. So I've got a function down here between line 27 and 30, called return true, that's gonna return a Boolean result. Now in order to make sure that we know that it executed, I'm printing out returning true here, and then I am returning the true value. So what happens when we call this function is it's just going to be replaced with the Boolean true in our application code. Now, up here in line 10, I've changed our or test to include this return true. And this doesn't make sense in our demo example. But I'm doing this
just to show you how this works. So in this case, we're ordering three tests together. So what's going to happen is go Is going to evaluate these guys, it's going to generate a result. And it's going to take that result, and it's going to order it against this guy. So basically, it's going to evaluate all of them. And only one of those has to be true in order to generate a validation message. So as you might expect, if we run this like this, we're going to get returning true because the guest is not less than
one. So we're going to check this value, this returns true. And so we're going to evaluate if it's greater than 100. None of those are truths or or statement fails. And then we're going to drop down and we're going to execute this code down here. Like we might expect, if we generate an invalid value. So if we put a negative five, then well, let's just run it and we'll see what happens. So if we run this, we get the validation message. The guests must be within one and 100. Well, that's okay. But what happened to
our return true? If you remember, this function is supposed to print returning true out, and it didn't. So what happened? Well, what happened is a concept Called short circuiting. So as soon as one part of an or test returns true, then go doesn't need to execute any more code, it already knows that the old test is going to pass. So it does what's called short circuiting, which basically means it's not going to evaluate any other part of the or test, it's just going to move on and say, well, the or test passed, and therefore everything
works. So in this case, since guess is less than one, there's no reason for go to evaluate these two tests here. So since this test is a function call, it doesn't even execute that function. Now, if we go the other way, and we go out of range high, then this is going to return false. This is actually going to return true because it's hard coded. And actually go isn't even going to evaluate this because this is going to return true. However, this value is going to fail this test here. So if we run this, we
see that now we get returning true printed out. So this is a concept called short circuiting. Go is going to lazily evaluate the logical tests that we put in here. So when we give It negative five, since it doesn't need to evaluate anything after this first test, it doesn't. So any function executions are not going to be evaluated. The same thing happens for an and test. If one of the parameters returns false, then we're going to get a short circuiting here. So if we get into a situation where this is false, go will not even
evaluate this test here. So for example, with the negative five, when Google evaluates this, it sees that the guest is not greater than or equal to one. So the ant test has to fail because both sides of an ant test have to return true in order to work and so go is going to exit early It's not going to execute this code. So for the next thing that I want to talk about, I want to actually drop back into our baseline for our test here. And I want to talk a little bit about these two
if tests. Now this code obviously works, but it's a little bit ugly, right? Because we have this logic here, guess is less than one or guess is greater than 100. And then we have this expression here. And this is exactly the opposite. So this is basically saying, Well, if it's not less than one or greater than 100, then do this other thing. So while this code is perfectly fine, it becomes a little bit of a maintenance nightmare, because really, our intent is that we want to execute either this code or this code. So the way
we can do that a little bit more elegantly and go is by taking out all of this and just putting in the keyword else. So what's going to happen in this situation is it's going to evaluate the logical tests. If these tests return a Boolean true, then we're going to print this. Otherwise, we're going to execute this block here. So in this case, if we run this, we see that we get the value too low. And our logical tests down here in line 21. Execute. If we put in the value negative five, however, then we
get the same behavior that we had before. But our code is a lot cleaner, and a lot more clear as to our intent. Now related to that is the situation where we have multiple tests that we want to chain together. And so let me paste in another permutation of Our little game. And in this case, I've actually split out the validation logic. So I want to check if it's less than one, I want to print out one message, if it's greater than 100, I want to print out another message. Otherwise, I've got a valid value.
And to do that, I'm using this else if clause. So basically, what this is doing is it's taking an IF test, and then it's chaining on another if test if this fails. And so what goes is going to do is the first logical test that passes is going to be executed, otherwise, it's going to drop down to the else clause. So with the guests of 30, we would expect our else clause to fire. So if we run, we see that in fact it does. If we put in a value of negative five, then we would
expect this first statement to run and it does. And if we put in a value that's too high, then we see that we get the second validation message printed out. And those are the three different ways that we have to create if statements. So we've got the simple if statement, that's just if and if that evaluates to true, then The statements inside of the curly braces are going to execute. We've got if else. So we're either going to execute the first set of code or the second set of code. And then we have the if
elsif, which we can chain together with else in order to have very complicated branching, so that we can have multiple code paths execute, depending on how our logical tests execute. Now one thing to keep in mind is through these examples, we've never been able to have more than one code path execute. So even if we did something like this, if we did elsif guess is less than one or the notice both the first test and the second test will pass with a value of negative five, however, goes on and go to execute the first branch
that has a succeeding test. So we get the result, the guests must be greater than one because this test passed. And so this test wasn't even evaluated, go immediately went in and executed this branch of code and ignored all of the rest of our if statements. Now another thing to keep in mind when you're working with equality operators, and this Isn't specific to if checks, but it is something that's easy to demonstrate with an F check. So if I take a look at this example, here, I've got a floating point number of 0.1. And I'm
going to check to see if that number is equal to the number squared, and then I'm going to take the square root of it. So if you get out of pencil and paper, you're going to take point one squared, which is going to be point one, and then you're going to take the square root of that, which is going to be back to point one, right? So this should evaluate to true. If we run this, we see that in fact they are however, if I add in a couple of decimal places here and run this,
now go says the results are different. Well, the fact is that if you square a number and take the square root of it, it is the same number. So what's going on here? Well, the thing is go is working with floating point numbers and floating point numbers are approximations of decimal values. They're not exact representations of decimal values. And so we have to be very careful with them. When we're doing comparison Operations with decimal values, this is generally not a good idea, the better approach is to generate some kind of an error value and then
check to see if that error value is less than a certain threshold. So in this case, since we're taking a floating point number and comparing it to a floating point number, what we can actually do is divided two numbers and subtract one. And now what we have is a value that's close to zero. So if we take the absolute value of that, so we'll use the ABS function, which does that from the math package. And then check that to see if it's less than, for example, point 001. What this is doing is saying divide these
two numbers, and if they're within a 10th of a percentage of each other, then we're going to consider them to be the same. So if we run this, then you see that the results are the same and we We can add as many decimal places as we want right now. And the errors that are associated with floating point numbers aren't going to get in our way. Now, this isn't a perfect solution, because you could get two numbers that aren't Truly the same, and get them to pass this test. So the key to that is tuning
this error parameter here, making sure that it's sufficiently small to catch those cases, but sufficiently large, so that the errors introduced with floating point operations don't affect your results. Okay, so the next thing that I want to talk about are switch statements. So let me start that conversation by dropping in an example. And we'll see a basic switch statement. So a switch statement is a kind of special purpose if statement. So where if statement has a logical test and runs a block of code if that test is true, and then you can add an elsif
to run a second test and another elsif. And as many else ifs as you want. A lot of times in application development, we run into a set of comparison operations that are very similar to one another. So a lot of times it's more efficient to use a switch instead of an if statement. So we have a simple example here, we're going to use this switch keyword as you might expect. And in this example, we're going to start with this value here. And that's called a tag when we're working with switch statements. So this is what
we're going to compare everything against, then we're going to have a series of cases. And you can see here we got case one, and we got case two. So what's going to happen is the value here is going to be compared to the tag. And if it's true, then the statements after the case are going to execute. If not, then they're not going to execute. And just like with if statements, the first case that passes is going to be the one that executes, we also have this default case down here. And that's going to execute
if none of our other test cases passed. So if we go ahead and run this, we see that we get to print it out. If we add the value one here, we get the value one printed out. And if we have three, then we get the default case printed out. So it's a very simple way to compare one variable to multiple possible values for that variable. Now in a lot of languages, if you want to compare against a range, then you'd have to do something like This, you'd have case two, you'd have case three. And
then we use what's called falling through in order to compare multiple cases together, well, we don't actually have falling through as a default behavior and go, but what we do have to make up for that is the ability to have multiple tests in a single case. So if I extend this example out here, and I say this is going to test one, five, and 10. And this is going to test two, four, and six, then I'm going to say this is going to print out one, five, or 10. And this will print two, four, or
six, and then I put in the value five, and let's just change this message to this is going to say another number. Okay, so if I run this, then I get the first case is going to pass because I matched this second test. So this is going to execute if I have the value one, five, or 10. So I can also get into that same case by passing in the value of one, if I pass in the value of four, then like you might expect, I'm gonna have the second case, evaluate. And naturally, if I
pass in an unknown number, then I'm going To have the default case execute, just like I had before with a single test. Now one thing we have to be aware of is the test cases do have to be unique. So if I try and run this, where I'm going to have five in the first case, and in the second case, that's actually a syntax error, because you can't have overlapping cases when you're using this syntax. Now, just like with if statements, we don't have to have a simple tag here, in our switch, we can use
an initializer, just like you're seeing here. Now, in this case, the initializer doesn't have to generate a Boolean result, because we're not doing a Boolean test in our switch statement up here, the Boolean test is comparing the second parameter in the switch statement to our cases. So in this case, we're just going to initialize that value. So I'm doing some simple math here, I have set equal to two plus three. So obviously, that's going to be five, then I've got the semi colon and the tag that we're going to be testing against. So as you
might expect, when I run this, the first case executes because Two plus three is five, five is matched by the first case. And so we get this statement printed out. Now another syntax that we have available to us in switch statements is a tankless syntax. So both of the styles that we've been talking about, we've always had a value that we're comparing to our test cases, where there's actually another syntax, this tag list syntax. And this is arguably more powerful than the tag syntax, although it is a little bit more verbose. So in this case,
on line eight, I'm establishing a variable which is going to come from some other logic in my application. And then I've got the switch statement that standing all alone and immediately opening a curly brace. So notice, I don't have a tag here. However, my case statements now I've got full comparison operations. And I can use the comparison operators, I can use the logical operators. So in this case, the cases are standing in for the logical tests that we have in our if statements, they're doing exactly the same job. So in our first case, I'm checking
if i is less than or equal To 10. And if it is, I'm going to print that out. And then in our second case, I'm going to check if i is less than or equal to 20. And then of course, we've got the default case. If The value falls through. So if I run this, I see that I do get the code executing, which is an interesting thing. And it executes the first case. Now, why is this interesting? Well, if you notice the first case, in the second case overlap, because 10 is less than or
equal to 10. It's also less than or equal to 20. So unlike the tag syntax, we've got multiple test cases. And I said they cannot overlap. When we're using this tag list syntax, they are allowed to overlap. And if they do, then the first case that evaluates to true is going to execute. Now another thing that I should have pointed out earlier is notice that we don't have curly braces around this, any statements that after this case, and before the next case is going to be part of the block that's going to execute, so I
can have as many statements as I want here. The delimiters, in this case, are the actual case keywords, The default keyword, or the closing brace, so any of those delimit the end of a block in a case statement. Now again, if you're coming from another language, then this syntax might be looking a little weird, because you might be looking for this word coming in here. And we'll have to add this word here. And we'll have to have this word here. So you might be wondering where the heck are my break keywords, one go, the break
keyword is actually implied, because so many use cases for switches have breaks in there. And forgetting a break is the cause of innumerable errors when working with switch statements, the design decision was made to have an implicit break between our cases, instead of an implicit fall through which most other c based languages have. So what happens if you do in fact want your case to fall through. So in this example, let's just say that we do want to print out that we're less than or equal to 10, and that we're less than or equal to
20. And we know that if we pass this first case, we're going to pass the second case to, in that case, We can actually use the keyword fall through. So if I run this, you see that both messages print out. So I'm going to get less than or equal to 10 and less than or equal to 20. Now one thing to keep in mind here is fall through is logic less. So when we say fall through to the next case, it will do that. So if I, for example, change this to be greater than or
equal to, then we know that I fails the second case, but if I run, it still executes, because fall through means that you are taking the responsibility with the control flow. And you intentionally want the statements in the next case to execute. Now and go you don't use follow through very often, because we have that tag syntax that can take multiple tests per case. And so a lot of the use cases for falling through are taking care of that. However, if you do have a use case where you need follow through, then it is available
to you. The next thing that I want to talk about was switch statements are a special use of a switch statement called a type switch. So I'm going to drop in this example here. And this should be pretty familiar except for what we're switching on. So notice that we are switching on a tag. But the tag variable is typed to type interface, which can take any type of data that we have in a go application. So the interface type can be assigned to an integer like we're seeing here, we can assign it to a string
to a Boolean, to a function to a struct to a collection, anything we want to assign to an interface is going to be permissible. So then in go, we can use this syntax here. So we can put in the dot operator. And after that, put in params with the word type in the middle. And what that's going to do is tell go to pull the actual underlying type of that interface and use that for whatever we're doing next. Now, you don't just use that for type switching. But this is a common use case for it.
So in this case, I'm going to assign the variable i the value one, which as we know is going to type I to be an integer. And then I can actually use go types as the cases, because this is going to return a type, then I can use those type keywords here as My cases. So if I run this, knowing that one is an integer, I should not be surprised when I see the first case passes, and the other two are ignored. If I had a decimal point zero here, then that's going to turn this
into a floating point 64. And we see that go recognizes that. Similarly, if I put one in a string here, then we're going to see that go understand that. And I can even make, for example, an array. And we don't need to initialize any values here. But if we see that, then go recognizes that as another type. And if I want to have a case to catch that, then I can actually have a case for a raise of three integers. And then I can print that out. Now if I run, we see that it recognize
that and this is different than two. So if I use a two element array, please be aware that those are different types. Arrays are not equivalent to every other array, you have to have the same data type and the same array size in order for them to be evaluated as the same type. Okay, the last thing that I want to talk about I can illustrate with this Example here. Now in this example we're extending on the previous one was type switches. And notice that I've got these two print statements here on line 11 and line
12. So if I run this, I see that I is an integer and this will print to it. In some situations, you might need to leave a case early. So maybe inside of the case, you're executing some kind of a logical test, and you find out that there is some logic in the case that you don't want to execute in certain situations. Well, if you want to do that, you can actually exit out of the switch statement early by using the break keyword. So if I add that and run, then I have a way to
break out of the switch early. And I'm not going to execute any code below that break statement. So you can wrap this in a logical test to determine if you've got a validation error. For example, on some incoming data, you might not want to save that data to the database. And then you can use this break keyword in order to short circuit that save operation. Okay, so that pretty much wraps up what I have to talk about with If and switch statements. Let's go into a summary and review what we've talked about. In this video,
we started our discussion about the different control flow tools that we have available to us in our NGO applications. And we started out by talking about the very basic if statements and how we can use a boolean variable to determine if a block of statements will execute or not. We then talked about the initializer syntax, and how that allows us to execute a statement, that's going to set up variables that we can use to run our test against as well as use within the statements of our if tests. We then talked about the comparison operators,
and we talked about how there are six of them, we've got less than, greater than, less than or equal to and greater than or equal to, to work with primarily the numeric types, as well as the double equals operator and the non equals operator that are available to be used more generally, we talked about the logical operators, the AND operator, the OR operator and the non operator that allows to chain together Boolean operations, as well as reverse the result of a Boolean operation. In the case of that knot operator, we talked about short circuiting and
how when you're chaining together tests with an OR operation, for example, if any one of those logical tests evaluates to true, then the remaining tests are not executed. So any side effects or any functional operations that might occur with those are not going to be executed. Similarly, with an an test, the first part of an ad test to return a false result is going to halt the execution of that and test. So any side effects you might be relying on afterward are not going to occur. We then talked about the two different variants of if
tests, we talked about the FL statement, and how we can use that to execute one of two branches of code. So if the if test succeeds, then we're going to execute the first block of statements. Otherwise, we're going to execute the second block of statements. Related to that is the if else if statement. And in this Case, instead of determining between one of two paths, and forcing your code to execute one of those, you can add additional logical tests. So at most one block is going to execute. But if all of your F tests fail,
then you might not get any blocks executing. And finally, I took a minute to talk about equality operations, especially when working with floating point numbers. And how equality and floats don't really get along that well. So if you do need to do some kind of inequality operation with floating point numbers, then you're going to want to turn that into some kind of a comparison operation against an error function. So for example, we were checking to see if two floats were equal, instead of checking to see if they're actually equal, and we divided them subtracted one
from that result, and check that against an error parameter. So we decided, if they were within a 10th of a percent, they were going to be considered the same, you can use a similar type of error function with your operations. But in general, you should not test to see if two floating point numbers Are equivalent to one another. We then moved on to talk about switch statements, and the various ways that we can use those in our applications. We started by talking about how to switch on a tag or tag is just another name for
a variable. But that variable has a special significance in a switch statement, because all of your test cases are going to be compared back to that tag. We talked about how with go we can have multiple tests with our case statements. And by doing that, we can actually eliminate a lot of situations in other languages where we need to fall through and chain multiple cases together. Well in go, we can just add those multiple tests in the same case, and eliminate the need to do that following through. We also do have initializers available with switch
statements. But instead of the initializer, generally generating a Boolean as we do with an if statement, the initializer is going to generate the tag that's going to be used for the comparisons. In our cases, we talked about switches with no tags and how it adds a lot of flexibility to the Switch statement. But it is a little bit more verbose syntax. So instead of the cases being compared for equivalency with the tag, we're going to actually put in the comparison operation ourselves. So we can use more than just equivalency testing we can check less than
or equal to, or any other comparison function, including chaining comparisons together by using logical operators. We talked about how we have the fall through keyword available in switches, and how that replaces the break keyword in many languages. So where many languages have implicit fall through an explicit breaking in go we flip it around so we have implicit breaks. But if you do need one case to fall through to the next Then you can use this fall through keyword. Now one thing to keep in mind when you're doing that is when you're falling through, any additional
case logic is not executed, the fall through will override any case logic that you have. And so the next statement will execute regardless of if that case would normally pass or not. We also talked about type switches, and how we can use that Special syntax by adding a dot perenne type perenne on an empty interface in order to get the underlying type of that empty interface. So for example, if we've got an empty interface that's holding an integer, then we can use that syntax in order to get that underlying integer value. Then in our switches,
we can switch against those types. And then we can make a decision based on the data that we got in. And finally, we talked about breaking out early, when we've passed into a case in our switch statement. There are times when, in the middle of the case, we need to exit out early, because we've done some additional analysis. And we've decided that we should not proceed executing this case. So for example, we've got a switch statement. And inside of that case, we're doing some validation on the data before we save it to the database, we
might decide that data is invalid, and we should not proceed to save it to the database. So in that case, you can use the break keyword break out of the case and continue execution after the switch statement, I want to continue Our discussion of the control flow structures that we have available in the go language by talking about looping. Now in the last video, we talked about the two ways that we have to introduce branching logic into our application if statements and switch statements. Well, when we talk about looping and go, things are actually going
to be a little bit simpler, because we only have one type of statement that we're going to have to learn. And that statement is the for statement. Now we're basically going to break this conversation down into three different parts, we're going to talk about simple loops, we're going to talk about how we can exit a loop early. And then we're going to talk about how we can loop through collections. Okay, so let's go ahead and get started by learning how to do some simple loops in go. So in order to start our discussion about looping
and go, I want to start with this perhaps the most basic for loop that we can create. So we're going to start with the for Keyword as you might expect. And then we're going to follow the for keyword with three statements, the first statement is going to be an initializer. Now this is the same as an initializer. And if statements and switch statements. So we can add any kind of statement that we want here. And normally, we're going to use that in order to set up a counter, when we're using a for loop like this,
the next statement that we have here is going to be some kind of a statement that generates a Boolean result. And that's going to be used by the for loop to determine if it's done looping or not. And then the last statement is going to be the incrementer. And normally, we're going to use that in a situation like this to increment a counter variable. So if we run this, we see that we get the numbers zero through four printed out, we could also increment i by two. And there, you see that we only get three
iterations of the loop, because I has the value zero, then it has the value two, and then it has the value four, when it comes to the next time it has the Value of six, well, six is not less than five. Therefore the comparison operation results to false and we exit out of the loop. So this is certainly possible to do and you will occasionally see this. But the most standard way that you see these loops created is like this using an increment statement. Now if you're coming from another language, you might be expecting to
be able to do something like this. So maybe we want to variables, increment in our for loop. And so we're going to initialize j there. And then we're going to increment j with each loop. So if we go ahead and try and run this, we see that that's actually not allowed, because we have no comma operator in the go language. So you can't separate two statements like this using the comma. So this is an error, and this is an error. So if we want to do that, though, we can use gos ability to initialize multiple
values at the same time by adding a comma here getting rid of this part here, adding the comma there. And then we can't treat these as individual statements, because you can only have one statement here. And In go the increment operation is a statement, it's not an expression. So we need to figure out a way to do both of these at one time. So we will simply come in and said I n j equal to i plus one and j plus one. And go ahead and run that. And now we have I printing out if
we have j here we see that j printout as well, we can actually increment j by two here. And we see that i and j are incrementing independently. And we have no problems here. Now just to come back and revisit that increment comment that I made earlier, you might be tempted to do something like this, if you just want to increment them by one. If we run that, we're actually going to get an error because again, in go unlike a lot of languages, the increment operation is not an expression. So we can't use that as
part of a statement. It is a statement on its own. So when you're just incrementing one variable, you can certainly do this because i plus plus is a statement, but we can't use that in combination with anything else. We can't use it as an expression. Something else that's interesting. Let me Go back to our initial example here. So we can play around with this a little bit. So at first blush, if you just come into programming, this AI variable might seem like it's something special because it's used up in the for loop. And the fact
is, it's a variable just like anything else. So we can do whatever we want with it. As a matter of fact, just to show you that, let me drop in this cool little bit of code. As you can see, what we're going to do here is we're going to take the modulus of i n two, and we're going to see if that zero, so basically, we're checking to see if it has an even number or an odd number. If I is an odd number, then we're going to divide it by two. If I is an
even number, then we're going to multiply it by two and add one to it, it's just a couple things to play around with the incrementer. So let's run this and see if this actually goes to completion. And in fact, it does. And you see that the variables going all over the place. So we start with zero zeros on so divide that by two, which stays zero, the next iteration Is going to be one because we're incrementing by one, so it's actually going to be multiplied by two and one added to it. So we print the
one here. But then by the time we're done here, two times one plus one is three, and so is three, that gets incremented to four, so we're printing for four out here, four is even to divide that by two, two gets incremented again, so we print out the value three, three is odd. So three times two is six plus one is seven, seven is greater than five. And so we leave the comparison operation, so we can do whatever we want with the counter. Now, just because you can play with the counter doesn't mean it's a
good idea. As a matter of fact, normally, it's very bad practice to manipulate the counter within the for loop. So maybe the best reason to show you that you can do this is to make sure that you avoid doing this because there can be times when you're inadvertently changing the counter variable within a loop. And your loops might start to act really strange, like we're seeing in this example here. Okay, so let's go ahead And restore our original example, make sure that that's running again, and it is we're printing out the values zero through four
again. Now the next thing that I want to show you is that we don't actually need all three statements here. So one thing that we could do, let's just say that I is initialized somewhere else in our application, so we don't need it to be set up here. And we can run with this first statement being empty. Now you can't leave this for a semi colon out, because if you leave that out, then everything's in the wrong place. And it thinks that this is going to be the initializer. And this is going to be the
comparison operator. So if we try and run this way, we're going to get an error. But if we leave the semicolon in, then go recognizes that we simply don't have an initializer in this for loop. And it executes everything the way that we expect it to. And of course I've variable ri is taken in just like we would expect. Now the difference between this format and the previous one is in this i is scoped to the main function. But when we have This syntax here, I is scoped to the for loop. So if I tried
to print the value of i out here, we're not going to get a valid value because I is not defined. But in this other form, when we're putting it out here, then I is scoped to the main function. And we are actually going to get the final value of I printed out which is the value five, because five is the first value that fails this test. So this is actually coming from this statement right here. Okay, the next thing that we can do, let me go ahead and wipe this out, we also don't need the
incrementer value, so we can eliminate that. Now if I run this right, now, we're actually going to get an error, because what this is going to do is it's going to generate an infinite loop. And in the go playground, they don't let us do infinite loops. And so after it runs for a little bit of time, it's going to shut the process down on us. If we were actually running this in a local environment, all you'd see is the value zero printed out over and over and over and over again, until eventually you got bored
And shut the application down. Now we can of course, since I is a variable just like any other, we can put the incrementer in here, and everything works just fine. So once again, we have to remember to put the semicolon here, if we remove the semi colon, then go doesn't know what the heck we're asking it to do, because this is invalid syntax. So if you have the first semicolon, you need the second semi colon. Now this case does happen actually quite a bit where we need a for loop and just a comparison operation. And
this is how go does do while loops. So in a lot of languages, you have two more keywords, you've got the do keyword and the wild keyword. And normally those work with simply a logical test. And then you have some other increment operations. So you either have an increment or inside of the loop, or you're pulling the next value off the stack. And comparing that on each iteration, or something like that, we'll go has the same problems to solve, but didn't make sense for the designers of the language to introduce a new keyword just for
what is Basically a special case of a for loop. So what they allow us to do is we of course can use this double semi colon syntax here. But you can also leave them both off. So if I run this way, we see that this works. And this is going to work exactly the same way as if we have both of these semicolons here. So this is a little bit of syntactic sugar. It's exactly the same construct. It's just you don't need to have the additional semi-colons and it reads a little bit cleaner. So in
this case, our for loop is just doing a comparison operation. Go is going to assume that we're taking care of where those variables for the comparison are coming from somewhere else. Now the third form of a for loop that we have available when we're working with counters like this is the infinite for loop. Because we also have a situation with do while loops in other languages, where we need the application to run through the loop and undetermined number of times. And by undetermined means, we don't actually know when to stop by an obvious logical test,
we need some complex logic within the Loop in order to determine when to leave. So in go, the way that we do that is we simply leave off the logical test, and we run like this. So if I run this, again, as you might expect, the playground is going to crash out on us because there's no way for the program to exit. And so it's just going to run forever. So we need some way to tell our infinite loop when it's done processing and when to leave. And the way we're going to do that is
using the break keyword. And then Normally, the way that you do this is you'll put some kind of logical tests inside of the loop. So we can say, if I equals five, then we're going to use that break keyword. And we saw the break keyword, the first time when we were talking about switch statements, or the break keyword is actually more commonly used inside of for loops, especially this kind of infinite for loop here. So what we're doing here is if i is equal to five, so we're going to print out the values, zero through
four, then when I was fine, we're gonna break out. So if we go ahead and run this, we get exactly the Same values that we had before. Now when we do this, we actually leave the entire for loop. So execution of the loop stops, and we're done processing. Now another thing that we can do is use what's called a continue statement. So let me drop in an example that shows you that. So in this case, we're looping from zero to nine, because we're going to keep incrementing as long as i is less than 10. And
then we're checking to see if the value is even if it is even, then we're going to use this continuous statement here. And what this does is it basically says, exit this iteration of the loop and start back over. So what's going to happen when we have 00 modulates to is zero, so we're going to continue, which means we're not going to hit this print statement, when we have one, one minus two is one, so we're not going to hit this continue, and we're going to print out one, and then two is going to hit
the continue and three is not. So what we should see is we'll only print the odd numbers out. If we run this, we see that that is in fact what Happens. So continuous statements aren't used very often. But they can be very, very useful. If you're looping through a large set of numbers, and you need to determine within the loop whether you want to process a record or not. Now, to show you the next thing, I'm actually going to do this, this is actually going to print out a nested loop. So what we're doing here
is a pretty simple example, we're starting with the variable, I initializing it to one, and we're going to run as long as i is less than or equal to three, and then we're doing the same thing with J on the inside. So basically is going to be one, then it's going to loop through days from one to three, then is going to be two, it's going to loop through j 4123. Again, and then inside this inner loop, we're just multiplying the two numbers together. So if we run this, we see that we get all of
the permutations of i times j, where i and j go from the values 123. So we get all of these values printed out, and everything works the way that we expect. Now what happens if we want to leave the loop, as soon as we Get the first value that's greater than three. So what we want to do is something like this, so we're going to multiply it times j, we're going to repeat our logic here. And we're going to see if that's greater than or equal to three, if it is, then we're going to break
out of the loop. Now if I run this, we don't exactly get the expected result. And the reason for that is as soon as i times j is greater than three, it breaks out of the loop, but the loop that is going to break out of is the closest loop that it can find. So it's actually breaking out of this loop here. And this loop just restarts because there's nothing to tell it to stop. So you might ask the question, Well, how do we break out in the outer loop? Do we have to have more
logic here and check to see if i times j is greater than three again, and then break out? Well, the answer is, of course, now, we do have a concept called a label that we can add. So a label is put together something like this, we're going to start with a word and follow it with a colon. And we're going to left justify that In our code blocks. So once I have a label defined, I can actually add the label after the break keyword. And it basically describes where we want to break out to. So
since this labels just before this for loop, we're going to break out of the outer for loop. So if I run now, I get to the value three, three is greater than or equal to three. And so I break out of both the inner and the outer loop. The last thing that I want to talk about in this video is how we can work with collections with for loops. So we've seen this before we have a slice of integers. In this case, we're containing the values one, two, and three. And we've seen how we can
work with this as a slice so we can print this out. We run this we get no big surprise, we get the slice printed out with the values 123. But what happens if I wanted to work with each individual value? Well, we've seen how we can do this, we can pull out one value from the slice, and we get the value two. But how do we work with an arbitrarily sized slice. So I don't know how big this slice Is going to be at runtime. I want to be able to loop through all the items
within the slice. So the way that I'm going to loop through a collection is using a special format The for loop called a four range loop. So I'm going to start that with the four keyword as you might expect. And then what I'm going to get is both the key and the value for the current item in the collection. So I'm going to have two variables that I can set there. And then I'm going to set that equal to this special range keyword and then provide the collection that I'm going to range over. So what
this is going to do is it's going to look at the collection here. And it's going to take each value one at a time, give us the key and the value, and then we're going to be able to work with those values independently. So inside of this, let's go ahead and just print out the value of the key and the value and run that. And you see that we get the indexes for the items in the slice, and we get their values. So the indexes are coming first, those are the keys. And then the values
Are the actual values in the slice. And this is the only syntax, you have to remember when using the range keyword, you're always going to get this key comma value result coming out. So this syntax is going to work for slices and for arrays. So if we make this an array by just adding a size, here, we see that we get the exact same output, we can loop over maps. So if I pull back the state populations map that we use for a couple of videos now, and loop over that, once again, pulling up the
key and the value and ranging over the map this time, then if I print the key and the value, I'm going to be able to split that out and then get the state in their population printed out separately. And I can even use a string as a source for a four range loop. Because what we're going to be able to do here is pull each letter out of this string as a character and look at that. And we see that we do get each position. And now we get this numeric value, which if you've been
paying attention over the last few videos, you're going to remember that this is actually the Unicode representation for that digit. So we can cast it back to a string by just using a simple conversion operation. And we see that we get each letter printed back out as we would expect. The other type of data that we can arrange over in go is what's called a channel. Now channels are used for concurrent programming and go and that's a topic for a future video. So I'm going to leave that topic right now, I promise when we talk
about parallelism and concurrency and go, I'll revisit the for loop and show you how you can use for loops with channels, because there are some special behaviors that you have to be aware of when you're ranging over channels. Now, if I come back to this example, and print this out, you see that we have access to both the key and the value every time. Now, this is not always true, you don't always need access to the value. And this can lead to some problems. Because if I only want to print the keys out and run
this, this is actually an error, because in go, you have to use every variable that's declared. And so we got this Situation where we've got this variable v, but we're not using it. And so that's going to cause us a problem with our application. Now and go what you can do, if you only want to get the keys out, you can actually skipped the comma value. And when you run that, it's going to ignore the value. But what happens if you get into a situation where you only want to print the value out. So if
I restore this, and I want to want to print the value, of course, I get the same error. But I can't just say this, because go who's going to assign the keys to that. So what do I do? Well, in this situation, when you only want the values, then you can use that underscore operator that write only variable that we've seen a couple of times. And basically, that's going to give a hint to the compiler that we don't actually care about the key. But we need this in this first position in order to get the
value. So when we run this, we do see that we get those populations printed out. Okay, so that covers what I want to talk about with four statements and go, let's go into summary And review what we've talked about. In this video, we talked about the second major control flow construct that we have in the go language, the looping construct, and we talked about how we're going to use the for statement for all of the loops that we need to do and go. So we don't have to remember I do keyword and while keyword and
a for keyword, when we use the for keyword that's going to allow us to loop over every collection that we have. We started by talking about simple loops and how there are three basic forms for the simple loop, we've got this first syntax, which is the most basic, that's the initializer test incrementer syntax, and we're going to use that initializer to set us up in our loop. Normally, by initializing a counter variable, then we're going to follow that with our test, our test is normally going to look at the incrementer to see if it's at
a certain value that's out of range for our for loop. And then we've got an incrementer and that incrementer. His job is to move the incrementer along at every iteration of the Loop in order to move to the next case that we want to loop through. Now we also learned in this first syntax, we actually can leave the first and last statements on that out. But if we're going to use any of this syntax, we have to leave the semi colons in place. So leaving a semi colon out confuses the compiler, it starts to assign
things to the wrong places. And so you're going to get an error in your application. So if you need an initializer or an incrementer, then you're going to have to use this full syntax. Now the second syntax eliminates the need for the initializing the incrementer. And it assumes that the test conditions are being managed somewhere else. So in this construct, you only have the for test. And as soon as that test is false, you're going to exit the loop. The last construct that we have is the for loop on its own and that will run
indefinitely. Until somewhere within your four loop, the break statement is called. That leads us to how we can exit early from a for loop. And we have three concepts that we talked about with that, we've got The break keyword that will break out of the immediate loop that's executing and continue the application execution after that loop. The continue statement is similar to the break statement, except for eight doesn't break out of the loop, it just breaks out of the current iteration goes back to the incrementer, and continues execution of the for loop at the next
increment. Now we can also use labels in our application and combine those with break statements in order to break out of an inner loop. And that's typically how they're used in go. So for example, if you have a nested loop where you're iterating over two collections, and you need to break out of the outer loop, then you need to set up a label and then follow the break keyword with the name of that label so that Google knows where to break out to. And finally, we talked about how to loop over collections, and how the
collections that we have available, there's quite a few we've got arrays, slices, maps, strings and panels that we can loop over. But they all have this similar syntax, we're Going to use that for keyword, we're going to get a key and a value. And then we're going to set that equal to the range keyword followed by the collection that we're looping over. So the keys when working with arrays, slices and strings are going to be the index within that collection. So you're going to have that zero based index. With maps, we're going to get the
key from that map. And the values are what you expect them to be. They're the value for that current index. Now when we're talking about channels, it's going to follow very similar syntax, but channels have a little bit of a special interpretation for these. And we'll talk about those in a future module, I'd like to finish our discussion of control flow constructs that we haven't go by talking about defer panic and recovery. We'll start that discussion by talking about deferred functions, and how we can actually invoke a function, but delay its execution to some future
point in time. Then we'll talk about how an application can panic. So in this conversation, we'll talk about how go application can enter A state where it can no longer continue to run and how the go runtime can trigger that as well as how we can trigger that on our own. And then related to panicking, we'll talk about recovery. Now when your application starts to panic, ideally, you'd have some way to save the program so that it doesn't bail out completely. So we'll talk about if your application can be saved, how you can use the
recover function in order to signal that to the rest of your application. Okay, so let's go ahead and get started. In a normal go, application control flows from the top to the bottom of any function that we call. Now, of course, we can alter that a little bit by introducing branching logic so that we can skip some statements. And we can use looping that we talked about in the last video to repeat certain blocks of statements multiple times. But generally, a function is going to start at the first line and execute through until it gets
to the last one. So if we run this program right here, we see that it's no big surprise that we print start, then We print middle, and then we print end. And what we can do if we want to defer the execution one of these statements is proceeded with the defer keyword. So if I put that in front of this print line here, then run that, you notice that middle is actually printed after end. So the way the defer keyword works in go, is that it actually executes any functions that are passed into it after
the function finishes its final statement, but before it actually returns. So the way this main function is executing is its calling line eight and it's printing out start, then it recognizes that it has a deferred function to call and then it prints out end. And then the main function exits now when go recognizes that that function exits, it looks to see if there are any deferred functions to call. And since we have one, and then goes ahead and calls that, so deferring doesn't move it to the end of the main function, and actually moves it
after the main function. But before the main function returned. Now, if we put the deferred keyword in front of all of these Statements, then we'll actually see an interesting behavior. Because the deferred functions are actually executed in what's called lifepo order or last in first out. So the last function that gets deferred is actually going to be the first one that gets called. And this makes sense, because often we're going to use the deferred keyword to close out resources. And it makes sense that we close resources out in the opposite order that we open them,
because one resource might actually be dependent on another one. So if we run this like this, we actually see that we've reversed the order of printing this. So we're now printing and middle and start. And just to remind you, these aren't executing in the context of the main function they're executing after the main function is done, but before it returns any results to the calling function. Now, these are good theoretical examples. But in this situation, I felt it makes sense to get a little bit more practical example for you to look at. So for this,
we're actually going to need to go into Visual Studio code, because We're going to need to run a program that's going to make some resource requests through the HTTP package. And you can't do that in a playground. So if we look at this program, here, we're importing a couple of packages. Then we're using the get function from the HTTP package in order to request the robots dot txt file from google.com. Now this example you can actually find in gos documentation. I basically stole this as an example for how you can use the deferred function. Now,
as you can see, with this request, we're going to get a response and an optional error. And we're going to check to see if that error is nil. And if it's not, then we're going to log that out and exit our application. If it is not nailed, and we got a good response. So then we're going to use the read all function from the IO util package, what that'll do is that'll take in a stream, and that'll parse that out to a string of bytes for you to work with. So if we look at the
robots variable, here, we see that that is a string of bytes. And then we close the body of the response To let the web request know that we're done working with it, so it can free up those resources. Of course, the read operation can fail, so we're checking for that error. And then we're finally going to print out the value of the robots variable here. So if we run this application, by invoking it with the go run command, we see in fact that we do get the robots printed out, and everything worked out just fine.
So we got this entire response printed out to the console here. Now, one thing that we're doing here, and that the defer keyword can help with is handling this body close. Now, in this application, we only have one statement that's really being worked with between the request being made, and the body being closed. However, in many applications, you might have quite a bit of logic that's involved, that needs that body to be open and continue to work with it, maybe you're reading this stream one character at a time, and you're doing some pattern matching or
something like that. So you can actually end up with quite a few statements in here. So what can happen here Is that you might make this request and open up this resource up here. And then it might be dozens of lines later that you actually get around to closing in. Well, that introduces the possibility that you forget to close it. Now, it's going to be hard to find that it's going to be hard to remember to close the resource. And so you're introducing the possibility of bugs coming into your application. So what we can do
is add the defer keyword here. And then we can go ahead and move this up a line. So if we run this, then it looks like we're closing the resource, and then we're trying to read it. But if we run the application, we see that it works just fine. Everything comes out the same way it did before. Now if we leave the defer keyword off, then we do get an error because we closed the response body before we were done reading it. So what this allows you to do, and this is the most common use
case that I've seen for using defer is it allows you to associate the opening of a resource and the closing Of the resource right next to each other. So you see this pattern a lot where we're going to open resource, we're going to check for an error. And then we're going to close the resource. And you want to check for the error first. Because if there's error generated here, then you actually never got this resource. And so trying to close, it will cause your application to fail. But as long as you know that you have
the resource there, then it makes sense to do a deferred call to close it. Now, one thing I would warn you about is this is a pretty common pattern. And you're going to see this all the time. But if you're making a lot of requests and opening a lot of resources within a loop, then using the defer keyword to close, it might not be your best choice. Because remember, the deferred statements don't execute until the function itself executes. So if you've got a loop that's going to loop over a million resources, you're gonna have a
million resources open before all of those get closed at the same time when the function finally executes. So if you're Working with resources in the loop, then you might not actually want to use the defer keyword, you might want to explicitly close those resources when you're done with them. Or another option would be to delegate the processing of those resources out to another function and have that function, close the resource. That way, you're not keeping a bunch of resources open at one time and wasting memory. Now the next thing that I want to show you
we can do back in the playground. So I want to do that, because I think that's the most accessible environment that we have is this program here. So when I run this program here, what do you think is going to print out when I run this, now there's two lines of thought that you might have, the first line of thought is, well, I'm defining a is the string start. And so I'm going to print start out. But you might also realize the defer statement is going to cause this statement to print after the main function
is done. And before main is done, we've already changed the value to end. So you could make an argument that this is Going to bring start or end with the fact is that we're going to get the values start printed out. And the reason for that is when you defer a function like this, it actually takes the argument at the time, the defer is called not at the time the called function is executed. So even though the value of a is changed, before we leave the main function, we are going to actually eagerly evaluate this
variable. And so the value start is going to be put in here, we're not going to pay attention to this value as it changes. Now the next thing that I want to talk to you about in this video, is what's called panicking. Now in go, we don't have exceptions, like a lot of applications have, because a lot of cases that are considered exceptional and a lot of languages are considered normal in a go application. For example, if you try and open a file, and that file doesn't exist, that's actually a pretty normal response. It is
reasonable to assume that you might try and open a file that doesn't exist. And so we return error values. We don't throw Exceptions because that's not considered exceptional and go. However, there are some things that get a go application into a situation where it cannot continue and that is considered exceptional. But instead of using the word exception, which you has a lot of connotations because of its use and other languages, we're going to use another word and that word is going to be panic. And that's because our application can't continue to function. And so it's
really starting to panic because it can't figure out what to do. So if we take this example, here, I'm declaring two variables a and b, and setting a equals one and B equal to zero. Now, obviously, the answer of one divided by zero is invalid, we can't do that in a go application. So if I run this, we're actually going to see that the runtime itself will generate a panic for us. And the runtime errors printed out integer divide by zero. And then we get a stack trace letting us know where that error occurred. Now,
if you're going along and writing your own program, and you get to a situation where your program Cannot continue to execute, because of a certain state that gets thrown, then it makes sense for you to panic as well. So to do that, you're going to use the built in panic function like we see here. So we're printing out start, then we're using the built in panic function passing in a string, and then we're going to print out. And what's going to happen when we run this is very similar behavior to when we had that divide
by zero error a few seconds ago, but the error message that's printed out is actually going to be the string that we passed into the panic function. And then notice we get the same stack trace out, we do get start printing out. But of course, we don't get the string end printed out. Now more practical example. Now this will not run in the playground. But that's okay, because it probably wouldn't fail on the playground anyway, is this one here. So this is a very simple web application where you're going to use the handle function. So
we're registering a function listener, that's going to listen on every URL in our application. And then This is a callback that gets called every time a URL comes in that matches this route. And all this is going to do is print out the string hello, go. As a matter of fact, why don't I go ahead and do this over in Visual Studio code, that way, you can see what this program is going to do. So we'll go ahead and save this and we'll get it up and running. Now there's no errors and went ahead and
started, okay. So if I come in, and hit that URL, which is localhost 8080, that you see that we get the string hello, go printed out. There you go an entire web development course in one video. So we've got a very basic web handler that's printing out the string, hello, go. And that's coming from this line right here, where we're writing to this response writer, this response writer is basically giving us access to the response to this web request. And we're printing out the string hello, go. Now, what's interesting here is this error handler right
here. So the listening serve function returns an optional error object. So when might that happen? Well, one situation That can happen all the time, when you're firing up a web server is the port can be blocked. So if I open up another terminal here, and go ahead and try and run this, again, you see that we get our application panicking. And the reason for that is we're trying to access a TCP port that's already been blocked. And it's been blocked by the fact that we're running the application over here. So this is a situation that
happens all the time. Now, the listening serve function doesn't have an opinion on if that's a panicking situation or not, because he just tried to execute something. And it's going to return an error value saying, Well, that didn't work, it's reasonable to assume that listening serve can fail. And so it's not going to panic is going to return an error. Now, we're the ones writing the web application. And we know that if that doesn't work, the entire application gets brought down, and nothing happens. So in that situation, we decide that we're going to panic passing
out that error that comes from the listening serve method, letting whoever's Trying to start this up, know that something very bad just happen. And so this is a common pattern that you're going to see and go go is rarely going to set an opinion about whether an error is something that should be panicked over or not. all it's going to do is tell you, hey, this didn't work the way that you expected it to work. It's up to you, as a developer to decide whether that's a problem or not. So in this case, we say,
yeah, that is a problem, we fail to start our application. And so we do want to generate a panic. So what are we going to do in the situation that our application is panicking. And we can get ourselves to a situation where we can recover the application. So panics don't have to be fatal. They just are, if we panic all the way up to the go runtime, and the go runtime realizes wait doesn't know what to do with a panicking application. And so it's going to kill it. So we come back to this application here,
and run, we see that once again, we're getting this panic. But something I want to show you related to our deferred discussion Is this modification here. So I've added this line, a deferred print statement that says this was deferred. So if we run this application, something interesting is going to happen. We get stopped printed out. That's not a big surprise. But then we get this was deferred printing out and then the panic happens. And this is really important, because panics happen after deferred statements are executed. So the order of execution is we're going to execute
our main function, then we're going to execute any deferred statements, then we're going to handle any panics that occur. And then we're going to handle the return value. So there's actually quite a bit that happens in a function after it exits this closing curly brace here. So why is this important? Well, the first thing that's important is that the first statements that are going to close resources are going to succeed even if the application panics. So if somewhere up the call stack, you recover from the panic, you don't have to worry about Resources being left
out there and left open. So any deferred calls to close resources are still going to work even if a function panics. But the other thing that's really important is if I change my deferred function here to something like this, now, this is a custom function. And we're not going to talk about this for a couple of videos yet. But this is important in order to talk about this. So I need to jump this ahead in the queue a little bit. So what we're creating here is what's called an anonymous function. And an anonymous function is
simply a function that doesn't have a name. So nothing else can call this, this is defined at one point, and we can call it exactly one time, these parenthesis here are making that function execute. And this is an important thing to know about the defer statement, the first statement doesn't take a function itself, it actually takes a function call. So you want to invoke that function, otherwise, things aren't going to work properly. Now, inside Of this custom function, notice that we're using this recover function here. So what the recover function is going to do is
it will return nil if the application isn't panicking. But if it isn't nil, then it's going to return the error that actually is causing the application to panic. So in this logical test, we're checking to see if it's not nil. So if it's not nil, that means our application is panicking. and in this situation, we're simply going to log that error out. Now, what happens in this situation is the execution still stops at the panic. And let me import this log package before we run this. And we see that the application still execute. So we
got the string start printed out, then we printed out the error using the long package. But we didn't get this end function printed out. So it looks like our application is still dead. But recovered does have some important impacts if we have a deeper call stack than what we're dealing with right here. So let me drop in this example in order to show you. So in this case, we've got the main function, that's going to be Our application entry point, of course. And then we've got this other function called panikkar. And all this thing does
is it's going to print on the line that says it's about the panic, and then it's going to panic. And it's going to go ahead and recover from that panic using that deferred function that we saw just a moment ago, then we're gonna have this line here, done panicking. And then up in our main function, we're going to print start, we're going to call the panacur. And then we're going to print and so let's see what happens when we run this. So as you see, we get the start line printed out, like you would expect,
we see the about the panic string print out, then we panic, something bad happened, we go into our recover loop, because we're not going to execute this because our application panic. And so our panikkar function is going to stop execution right there and execute any deferred functions. And inside of that deferred function, we call recover. So in that recover, we're going to log out the fact that we have that error, and we're going to Log that error message out that we see here. But then in the main function execution continues. So in the event that
you're recovering from a panic, the function that actually recovers, still stops execution, because it's in a state where it can no longer reliably continue to function. And so it makes sense for it to stop doing whatever it was trying to do. However, functions higher up the call stack, those functions that call the function that recovered from the panic, they are still presumably in a situation where they can continue, because you recover function said that your application is in a state working to continue to execute. Now, this is a little bit limiting as well, because in
order to determine what that error is, you actually have to call the recover function, which basically means that you're saying that you're going to deal with it. So what happens if you get that error, and you realize that this isn't something that you can deal with? Well, in that case, what you're going to do is repainting the application. So all we need to do in order to do that is Just read through the error, or you can come up with another error string, whatever makes sense for you. But in this case, inside of the handler,
we're throwing a panic again, then we actually see that we don't get that end statement printed out our main function panics because we read through that panic. So we get the full stack trace of when the panic actually happened. And we see that we're inside a func one function, one is actually this anonymous function right here. And we see that we do get that stack trace printed out and we don't get the string and printed out. So if you're in a situation where you're trying to recover from a panic, and you realize you can't handle
it, you can feel free to re throw that panic, and the further management of that panic, higher up the call stack. Okay, so let's go into a summary and review what we've talked about in this video. In this video, we talked about the final two control flow concepts that we have to be aware of in go programming. Now, one could argue that differs and panics aren't really control flow Constructs, because they're not the traditional branching or looping logic that we consider. But they definitely do alter the flow of execution of our program. And so I'm
going to lump them together in this category. The first thing that we talked about was the use of the defer function in order to delay the execution of a statement until the end of a function. Now, these are useful to group open and close functions together. So if your open function is going to open up a resource, and you need to make sure that you close that, then you can defer the call to close that resource to make sure that you're freeing up the memory and not holding on to that resource any longer than you
need to. The one thing I would warn you though, is be careful loops. If you're opening and closing a bunch of resources inside of a loop, then you probably want to explicitly handle that close without the deferred keyword. Because when you're using deferred keyword, all of those resources are going to stay open until the function finishes execution. And that can cause you some memory issues, especially If you're dealing with a very large number of resources. If you've got multiple different statements inside of a function, then they run in life order or lastin. First out, so
the last deferred statement that you call is going to be the first one that actually executes. And again, this makes sense, because if you're opening a bunch of resources, and those resources are dependent upon one another, then you're going to close them in the reverse order that you open them, which is typically the right way to go. arguments in a deferred call are evaluated at the time the defer is executed, not at the time the called function is executed. And that's important to keep in mind because you could pass in variables into a deferred function
call. And it can be a little bit confusing if you change the value of those variables further on. And those aren't reflected in your deferred statement. So just keep in mind, when you call defer, the value of those variables are going to be captured at the time that that defer is executed, not at the time the function actually Executes. Now, something unplanned happens in our application, then we've got two ways that a go application can go it can return an error value, which is the normal case, or it can panic. Now, what you generally want
to do is you want to return an error value, because in go programming, we typically don't consider a lot of things that go wrong in an application to be exceptional events, or to be events that should cause the application to shut down. For example, a classic example of that is making an HTTP request, and you make the request and you don't get a response from that server. Well, that's something that happens all the time, no response is a valid response, it just happens to be an error, because you're looking for that resource, and you got
a 404 return back. So in that case, you're going to return an error value, you're not going to panic, the application panics are used when an application gets into a state that it cannot recover from, for example, you hand the runtime a divide by zero problem, there's no way for you to figure out how to Divide a number by zero, and so has no way to manage that. And that's a situation where the application is going to panic. So just to summarize, then, they occur when an application can't continue at all, don't use them when
a file can't be opened. Unless it's a critical file. For example, if you're opening a template that's used in a web application to generate your view layer, then that might be something that's worth panicking over. But if you're trying to open up a log file, and you can't get there, well, there's no reason to panic for that necessarily, you just need to return an error value and then inform somebody that the logs aren't available. Now a situation where you might want to panic is unrecoverable events. So if you're starting up a web server, and it
can't get ahold of the TCP port that it's supposed to be listening on, then that's probably a situation where panic makes sense, the function will stop executing immediately at the point of the panic, but the third functions will still Fire. If nothing handles that panic, then eventually the panic is going to go up the call stack, it's going to hit the go runtime, the go runtime has no built in handling for panicking situations. And so the program will then exit. If you do have a panicking situation that you feel that you can recover from, then
you can recover using the built in recover function. Now that function is used to recover from panics, it's only useful inside of deferred functions. And the reason for that is because of the behavior of panic. Remember, when an application starts to panic, it no longer executes the rest of that function, but it will execute deferred functions. So the proper place to use the recover keyword is inside of a deferred function, that's going to look for a panicking situation. And if the application is panicking, then it can go ahead and decide what to do with it,
the current function will not attempt to continue. But if you do recover, then higher functions in the call stack will continue as if nothing went wrong. Now, if that's not the behavior that you want, if You call that recover function, you look at the error that's returned from the recover function and you can't handle it. Remember that you can go ahead and rethrow that panic by calling the panic function again. And then the panic will continue to propagate up the call stack, I want to talk about pointers and how we can use them in the
go language. We'll start that discussion off by learning how to create pointers. And then we'll talk about something called dereferencing. a pointer, which is basically using a pointer to get at some underlying data. Then we'll talk about the new function. And then we'll talk about a special type in NGO called nil. And then we'll wrap up our discussion by talking about built in types and go that use internal pointers. And so their behaviors a little bit different than other types that you'll work with in your applications. Okay, so let's dive right in and learn how
to create some pointers. To start our discussion, I want to start as simply as I can. So I'm going to use this example here. As you can see him declaring A variable a and assigning it the value 42. And then I'm going to go ahead and print that out. So it should be no big surprise that when I run this, the value 42 gets printed out. Now, if I extend this application a little bit, and I create a variable b and assign it to the value of a and print both of those out, then again,
it's no big surprise that 42 prints out two times. Now since a and b are value types. When I put in this line here, what go is going to do is it's actually going to copy whatever data is stored in a and assign it to be. So we're not actually pointing to the same memory. And we can prove that by changing the value of a to, for example, 27, and then printing out everything again. And if we run this, we see that the value of a changes to 27, but the value of b stays at
42. Now we can change this behavior a little bit by having B point to a using something called a pointer. Now in order to demonstrate that, I'm going to actually change our declaration syntax a little bit and go to more of a long form syntax. And the reason for that is to Make things a little bit more clear about how the pointer notation works. So I hope you'll agree with me that this line eight is exactly the same as the previous one that we had, it's just a little bit more verbose. Now, if I want
to change B into a pointer, and use that same long form syntax, what I can do is declare the variable b. And then I'm going to declare it as a pointer to an integer. And the way I declare it as a pointer is by preceding the type that I'm pointing to with this asterisk here. So then if I want B to point to a, then I'm going to use a special operator called the address of operator that you see here. So at line nine is now saying is that B is a pointer to an integer.
And I want to point it to a now what is a pointer exactly? Well, let me remove these lines here and run this and see what we get as output. So as you see here, a is still holding the value 42, like we expect, but B is holding this strange data here. So what is that? Well, this is actually the numerical Representation for the memory address that is holding the location of a. So at this location in memory, we actually have assigned the integer 42. So be isn't holding the value 42, it's holding the memory
location that's holding his data. And we can prove that by using the address of operator down here. And when we run this, again, we should see, in fact, we do see that the values are exactly the same. So the address of A in memory is this value here, and B is holding that exact value. And as a matter of fact, we can even go the other way. Because while the address of operator is going to give us the address of a variable in memory, we can use a different operator in order to figure out what
value is actually being stored at a memory location. And that's called the dereferencing operator. So if I go ahead and remove this, and then I'm going to put a dereferencing operator right here in front of this pointer. Now you notice we're using the same asterisk. But these have a little bit different meaning here. So up here on line nine, this asterisk before type is Declaring a pointer to data of that type. So this is a pointer to an integer. Now when I put the asterisk in front of a pointer, then that's going to dereference, which
basically means it's going to ask the go runtime, to drill through the pointer, find the memory location that that pointer is pointing to, and then pull the value back out. So if we run this, we see that we get the value 42 printed out both times once again. Now what's the point of all of this, what the point is, since B is pointing at the same value of a, Now both of these variables are actually tied together. So if we change the value of a once again, and then print out their data, now we see
that both A and dereferencing, b both give the value of 27. Because they're both actually pointing at the same underlying data. As a matter of fact, we can even dereference the pointer and use that to change the value. So if I use the dereference, B, and assign the value 42 to it, and then print it out again, then we see that once again, both values are changing a and the value that B is pointing To our both teams, because it's in fact the same data. Now, if you come from a background in languages that allow
you to work with pointers as variables, then you might be expecting to do something called pointer arithmetic. So if I drop this example in here, we're going to start to play around with something that's going to lead us to see how go treats pointer arithmetic. So I'm going to start with a variable a, and that's going to hold an array of three values, one, two, and three. And then I'm declared B as a pointer to the first element in the array to this value right here. And then C is pointing to this second element right
here. Now if I use this print statement, here, it's going to print the value of the array and then this percent p syntax is actually going to print the value of the pointer for BNC. So if we run this, we see that we do in fact, get the array and then we get these two memory locations printed out that B and C are holding. Now notice that C is holding a value that's for higher than B. And the reason for that is how go lays out arrays In memory. Since this is an array of integers
and integers in this version of the runtime are four bytes long each element of an array are four bytes apart. So this memory address ending with 124 is holding the first element in the array, and then four bytes later, we're going to hold the next element of the array c you might be tempted to do something like this if you come from another language, if I take the address of C and subtract four, then that actually should give me the address of B and then I can dereference that and Both of these should be pointing
to the head of the array. Well, if I run this, I in fact, see that I get an error. And the reason for that is go does not allow math like this to be done on pointers. Now, once again, if you've come from C or c++, you're probably aware of the tremendous performance advantages that you can get if you're allowed to do pointer arithmetic, because you can jump around mapped memory very, very quickly, and gain some pretty substantial benefits in the performance of certain applications. However, whenever you Get into pointer arithmetic, you're typically getting into
some fairly complicated code. And since go has as one of its core design concerns simplicity, the decision was made to leave pointer arithmetic out of the go language. Now, if you absolutely need to have something like this in your application, then what I would suggest is you can come into the go packages and come down here to the unsafe package. And this is going to give you operations that the go runtime is not going to check for you. So if you really need to do pointer arithmetic and things like that, then the unsafe package, which
has a very appropriate name is available for you for those very advanced scenarios. Now, those scenarios are advanced enough that what I'm going to suggest is if you need to know it, you're going to learn it. But generally, you're not going to need to know how this stuff works. Now the next thing that I want to show you is how we can actually create pointer types. So we've seen this address of operator and that's allowing us to instantiate a pointer To a certain variable. So if we look at the type of B, it's actually a
pointer to an integer because we're pointing to one element in the array. But so far, we've had to declare the underlying type first. Well, that's actually not necessary and go because often, you only want to work with the pointers, and you don't really care where the underlying data is stored, you just need the ability to point to it wherever it's at. So in this example, we see how we can do that. Now we've seen almost exactly the same syntax before, because when we were talking about structs, we had an example that looks something like this.
So we were declaring my struct object, we were instantiating it using the object initialization syntax. And when we print everything out, we print the value 42 out. Now if I make this MSX variable, a pointer to a my struct, and then use the address of operator in this object initializer, then I actually get almost the same behavior, except for notice that when I print out the value of MS, I end up with this ampersand here, which is basically saying that ns is Holding the address of an object that has a field with a value 42
in it. Now the advantage of being able to do this, it's going to come to light in a future video. But for now, just be aware that you can do this. Now this isn't the only way that we have available to us to initialize a variable to a pointer to an object, we can also use the built in new function. Now unfortunately, with the new function, we can't use the object initialization syntax, we're just going to be able to initialize an empty object. So if I go ahead and run this, we see that we do
get an initialized object. But all of its fields are initialized to their zero values, we can't initialize them using the object initialization syntax. Now, since I mentioned zero values, it's important to understand the zero value for a pointer. Because as we talked about, in a very early video, every variable that you declare in go has an initialization value. So right here after line eight, ns is holding something. So the question is, what is that thing? So if I go ahead and copy this print statement Up here, we will be able to answer that question. So
let me format this and run it. And then we see that we get the special value nil out. So a pointer that you don't initialize is actually going to be an initialized for you. And it's going to hold this value nil. So this is very important to check in your applications. Because if you're accepting pointers as arguments, it is best practice to see if that pointer is a nil pointer. Because if it is, then you're going to have to handle that in a different way. So for example, if we try and drill through and get
to this foo field, but and this is actually nil, then we're going to get a runtime exception, and our program is going to crash on us. Now that actually leads us to an interesting point, how do we actually get at this underlying field and work with it? Well, the obvious way that we're going to need to do that is we're going to have to dereference the ns pointer in order to get it that struct, and then we can get it that field. So we're going to have to use something like this. Now, you might be
asking why the prints are there? Well, it turns out that the dereferencing operator actually has a lower precedence than the dot operator. So we need to print in order to make sure that we're dereferencing, the MS variable instead of dereferencing, Ms dot foo. So now we can go ahead and set this to the value 42. Now in order to get at the value of foo, to print it out, then we're going to have to repeat the same exercise. So we're going to add some brands here. I'm going to wrap this ms variable, and then we'll
print out the value of foods. So if I go ahead and run this, we in fact, do set and get the value 42, which is the value of that field. Now I hope you'll agree with me at this point that this syntax is really ugly, because we use this syntax every time we dereference a pointer, we're going to have to use match that a params and have this dereference operator. Well it turns out because of the limitations that are put on point Here's the compiler is actually able to help us out a little bit. So
in fact, we don't need this syntax at all. If we go ahead and remove this and remove This from the print statement as well, and run this, we actually get exactly the same behavior. Now again, if you're coming from a language, which makes extensive use of pointers, this is probably freaking you out. Because the pointer and s doesn't actually have a field foo on it. The pointer MS is pointing to a structure that has a field foo. So how is this working? Well, again, this is just syntactic sugar. This is just the compiler helping us
out because it understands or not actually trying to access the Foo field on the pointer, we're implying that we want the underlying object. And so go is going to go ahead and interpret that properly for us, and dereference, the pointer. So the compiler really sees this statement the same as this statement, they're exactly the same to the compiler, it's just one reads a little bit more cleanly. Now, the last thing that I want to talk about today is how go handles variables when they're assigned one to another. So let me go ahead and paste this
example in here. As you can see, on line eight, We're initializing an array. On line nine, we're initializing another variable and pointing it in the same array as a, then I'm going to print out both A and B. And then I'm going to change index one of the A array to 42, and print them out again. Now, we've already done this example in the past, and hopefully you remember that a is going to change but B is not because b is a copy of the array that we stored in a and so they update independently
of each other. However, if I remove this index, and turn this into a slice, the behavior changes a little bit. If we run this, now we see that both A and B are changed together. So what happened there? Well, the slice is copied just like the array, but the effect of the copying is a little bit different. Because in the version with an array, the values of the array are actually considered intrinsic to the variable. So A is holding the values of the array, as well as the size of the array and that size is
held that way we can do bounds checking. So for example, if we asked for index three, which is beyond the bounds of This array, and run, we can do bounds checking in the go language. And that's a very good thing. However, with slices, remember, a slice is actually a projection of an underlying array. And so the slice doesn't contain the data itself, the slice contains a pointer to the first element that the slice is pointing to on the underlying array. So what that means is that when we work with slices, the internal representation of a
slice actually has a pointer to an array. So while line nine is still copying the slice a into the slice B, part of the data that gets copied is a pointer, not the underlying data itself. What that basically means is, when you're sharing slices in your application, you're actually always going to be pointing at that same underlying data. Now, the other built in type that has this behavior is a map. Because maps, once again, have a pointer to the underlying data, they don't actually contain the underlying data in themselves. So if I take this example,
where I'm initializing a map of strings to strings and assigning that to a, and then B is assigned to a, then I print them both out, then I change one of the values in a and print them out again, if I run this, we see that both maps start out the same. And then when I change the key foo in the map, a we actually change that in the map B as well. So what does that mean? Well, what it means is, when you're working with slices and maps in a go application, you have to
be very, very careful to keep in mind at all times who's got access to that underlying data. Because passing slices and maps around in your application can get you into situations where data is changing in unexpected ways. However, if you're working with the other data types, specifically primitives, arrays, or structs, then this generally isn't going to happen to you because when you copy a struct, it's actually going to copy the entire structure unless you're using pointers. Okay, so that wraps up what I have to talk about with pointers. Let's head into a summary and review
what we've talked about in this video. In this video, we talked about pointers and how to Use them in the go language. Now, we haven't seen a lot of practical application for pointers yet. But we need to understand what pointers are. Before we get into that. So over the next couple of videos, we'll get into why you would want to use pointers and the benefits of them. But for now, we're just trying to introduce the basic subject. So we started by learning how to create pointers. And we learned that if we prefix a type with
an asterisk, that's actually going to declare that type to be a pointer to that underlying data type. So for example, we have this asterisk int, which is going to be a pointer to an integer. We also learned how we can create pointers using the address of operator to get the address of an existing variable in memory. Then we learned about dereferencing pointers and how we can use that asterisk operator again, but this time in front of a pointer instead of in front of a type and use that to drill through the pointer to get at
the value that the pointer is pointing to. We also learned that when you're working with complex types, such as Pointers to structs those pointers are automatically going to be dereferenced for us. So our syntax doesn't get cluttered up by a whole bunch of dereference, operations and parenthesis, then we moved on to learn how to create pointers to objects. And we learned that there's a couple of different ways. So the first thing that we can do is use that address of operator to get access to a pointer to an object that we've already created. So in
this example, we've got an instance of a my struct object called Ms. And then we can use that address of operator to create a pointer p to that struct. But we can also do that directly. So if we proceed an object initializer, with that address of operator, then we can actually directly create a pointer. And we don't have to have an intermediate variable that's holding that value, we can also use the new keyword to initialize an object, but we can't initialize the fields at the same time. So the behavior is a little bit different, because
we're going to have to use the new keyword that's going to zero out all the fields in a struct, for Example. And then we're going to have to come in later and initialize all the values. The last thing that we talked about was types with internal pointers. And we saw how while they're treated exactly as any other variable type, when we do an assignment, their behavior is a little bit different. So all assignment operations and go are copy operations. So whenever you have a variable a and you create a variable b and set it equal
to a, all the data in a is going to be copied in order to create B. However, slices and maps contain internal pointers. And so even though they're copying, they're copying pointers, and so they're pointing to the same underlying data, we're going to take a deep dive into functions and how you can use them in the go language. Now, we've been talking about functions throughout this entire video series. But we've never really taken the time to focus on them, because there's a lot of groundwork that we've had to go through building up to the point
where we can understand what a function is, and how we can use those in our applications. So in today's video, like all of our videos in this series, I'm going to break the discussion down into multiple parts. We'll start by talking about the basic syntax of a function. Then we'll talk about the parameters that you can pass into a function to influence how it works. Then we'll talk about the return values that you can get back out of the function. We'll talk about something called an anonymous function. We'll talk about how functions in the go
language are first class citizens and can be passed around in your application like any other variable, and then we'll wrap up our discussion by talking about a special kind of function called a method. Okay, so let's get started by learning the basic syntax of a function. To start our discussion of functions, we don't have to go any farther than the basic application that the NGO playground gives us. So as soon as you come into the playground, you're presented with this very simple application. And right here, we have our first function. Now, the way that a
go application is structured, is you always have to have an entry point in The application. And the entry point of go application is always in package main. And within that main package, you have to have a function called main that takes no parameters and returns no values. So we can see that right here. And so when we run the application, the application actually starts right here. And so we print out the string, hello playground. And that's all that our application has to do. So every NGO application starts this way. And we see here the most
basic function that we can create in the go language. So there are several major parts that we need to understand about a function. First of all, they start with the func keyword. So as you can see here, we're not going to start with function or anything else, we start with the func keyword, and that's going to describe a function that we're going to create. Then we have the name of the function that we're going to create. And this follows the same naming convention as any other variable and go see you're going to use Pascal case
or camel case for the names of your functions. And depending on if you Have an uppercase or lowercase determines the visibility of that function. So just like with variables, and uppercase first letter is going to be published from the package, so anything else can use it, and a lowercase function name is going to be kept internal to the package. Now after the name of the function, we have these match params here now we'll see as we get into parameters, what these are for, but this is required syntax after the main function. So even if you
don't take any parameters in your function, you have to have these match params. And then the actual body of the function is contained within these curly braces here. Now there are a lot of holy wars and a lot of languages about where these curly braces should go. So some languages put them here, some languages put them down here, some languages like to indent them, there's all sorts of different conventions about where to put these curly braces. Well, in the go language, there aren't any arguments because this is the convention that is enforced by the compiler
itself. So You have to put the opening curly brace on the same line as the func keyword. And then the closing curly brace generally has to be on its own. Now there are a couple of situations where you can have that closing curly brace combined with a couple of other params. And we'll talk about that a little bit later in this video. But generally speaking, when you're defining a function, the closing curly brace has to be on its own line. Now, with functions defined like this, the execution path is static, we can't actually influence what
our functions doing from the outside, because we're not passing any information into it. So if we do want to pass information into it, then we're going to provide what are called parameters into the function. So let me just drop an example that uses parameters. And you see here a main function is now calling into another function called say message. And that same message function takes in this parameter MSG, that's of type string. So when you're defining a function that takes parameters, the parameters go between these two parenthesis here. And They're described like any other variable
declaration, except for you don't need the var keyword. So you're going to have the name of the parameter, and you're going to have the type of the parameter. So then when we want to call that function, we have to pass in the value for that parameter. And that's called an argument. So we're going to pass in the argument Hello, go. And then inside of our function, we're printing out the message that gets passed in. And so when we run this, we see that Hello go prints out as a result. Now this MSG parameter isn't special
in any other way than the fact that it's passed into the function, it's treated as a local variable, just like any other local variable that we might create. So if we created another variable here, and we said, Hello, go, that's treated exactly the same way as our mystery variable. So the only difference between the two is the MSG variable can be passed in from the outside. And of course, that greeting variable was created locally. Now, you aren't constrained to just pass a single parameter in, you can Actually pass multiple parameters in. So if I drop
in this example, we can see that in action. So here, I've extended the same message function to take two parameters. So I've still got the message parameter that's of type string. And then I've put this comma here, and I've added another parameter, and that's going to be an index. And that's going to be of type integer here. So as you can see, we can pass as many parameters as we want, we're just going to provide a comma delimited list of those parameter names and their types. So then when we call the function, we're going to
pass in one value for each one of those parameters. And they have to be in the same order that they're declared. So the first argument that we're going to be passing is the string hello, go, which is going to match this MSG variable. And then we're going to pass in the I that's going to be the loop counter here. And that's going to be passed into this ID x parameter. So then, in this function, we're just going to print out the value of the message and then we're going To say what index we receive. So
when we run this, we see that the message prints out five times. And we get the same message, but the index variable changes because we're passing in a different value every time we call the function. Now, often, when you're defining a function, you're going to pass in multiple parameters of the same type. So you're going to be tempted to have syntax like this. So in this example, we've got a slightly different function. So instead of a generic, say, Hello, we're going to have a say greeting function. And we're going to provide the greeting that we're
going to have and the name of whoever it is we're going to greet. So in this case, we're passing in the string hello, and the string Stacy. So when we run this, we're gonna say hello to Stacey. Now, since these types are the same, the go compiler actually provides us a little bit more syntactic sugar, because this is a little bit more verbose than is strictly necessary. So instead of specifying the type every time, we can actually just specify a comma delimited list of variables, And then the type at the end. And what the compiler
is going to do is, it's going to infer that every variable that's in that comma delimited, list has the same type. So when we run this, we actually get exactly the same execution. But we just have a little bit more terse syntax. Now, so far, we've been passing parameters in as value types. If you remember from our last discussion, we were talking about pointers. And notice that I don't have any pointers in our function signatures right now. So let me drop in this example here. And then we can start playing around with the difference between
passing in values and passing in pointers. So when I have this example here, and I run it, we get exactly the same output that we had before. But I've got some variables that I'm using to pass in as arguments to this function. Now, what do you think is going to happen if I change the value of one of these variables inside the function, so for example, if I change the name variable to Ted, and let's go ahead and print that out, just to verify that it printed. And then What do you think is going to
happen if I print the same variable out, again, right here. So I'm passing in the name variable by value. So that means that the go runtime is going to copy the data that's in this name variable and provided to here. So what we would expect is when we change the value of the name variable right here, it should have an effect. And we should print 10 out here. But since this is a copy of the name variable, we actually shouldn't have any effect out here. So if we run this, we see that in fact, that
is true. So this is a safe way to pass data into a function, you can be assured by passing by value, that the data is not going to be changed when you pass it in. Now, if I change this to passing in pointers, by adding a pointer here, and then passing in the address of these variables right here, and then dereferencing, the pointers right here, then let's see what's going to happen. Actually, I need to add another dereference right here. And now we're passing pointers to our variables around in our application. So now, instead of
working with a copy of The name variable, we're working with a pointer to the name variable. And so when we run this, we actually see looks like I missed a dereference operation right here. And now we see that we have in fact, change the variable not only in the scope of the function, but in the calling scope as well. So by passing in a pointer, we have in fact manipulated that parameter that we passed in. Now, why would you want to do this? Well, there's a couple of reasons. First of all, a lot of times
our functions do need to act on the parameters that are passed into them. And so passing in pointers is really the only way to do that. The other reason is passing in a pointer is often much, much more efficient than passing in a whole value. Because right now we're passing in simple strings, and strings aren't that large and ghost, so passing in copies versus passing in pointers is pretty much going to be the same in terms of performance. However, if you're passing in a large data structure, then passing in the value of that data structure
is going to cause that entire data structure To be copied every single time. So in that case, you might decide to pass in a pointer simply for a performance benefit. Now, you do have to be a little careful when you're passing in pointers, because of course, you can inadvertently change that value. And so you can cause some issues for yourself. Now, just to remind you of something else that we talked about in the pointer discussion, if you're working with maps or slices, then you don't really have this option, because since those two types have internal
pointers to their underlying data, then they're always going to act like you're passing pointers in. So just be careful when you're using those data structures. Because you can inadvertently change the data in your calling function, when you're manipulating them within the calling function. The last thing that I want to talk about when we're working with parameters are what are called variadic parameters. So if I drop in this example, here, we can see an example Of a variadic parameter. So in this case, I've got a generic some function that I'm creating here, and I'm passing in
the numbers one through five. Now, I'm not receiving five variables here, instead, I've got one variable here, and I've preceded its type with these three dots here. So what that's done is that's told the go runtime to take in all of the last arguments that are passed in, and wrap them up into a slice that has the name of the variable that we have here. So then inside of the sum function, we're going to print out what that values object is just so we can see that and then we're going to go ahead and add
up all the values in there. So since it's going to act like a slice, we can use a for loop and range over those values. And then we're going to print out the result of that. So when we run this, we see that we do in fact, have a slice is printed out, the sum is 15. So we got that result printed out properly. And there's no problem at all. Now, when you're using a variadic parameter, you can only have one and it has to be the last One. So if I, for example, want to
have a custom message string, I can pass that in, and then pass that in here and then replace this. And this still works just fine. However, I couldn't, for example, put the message parameter as the last parameter, because the runtime doesn't have the ability to understand where the variadic parameters and and where additional parameters would begin. So if you're using variadic parameters, you can only have one and a half to be at the end. Okay, now, it's nice to be able to pass data into our function, because now depending on the different data that I
pass in, I can change the behavior of the function. But it's also very useful to be able to use our functions to do some work, and then return a result back to the calling function. So in order to do that, we're going to use what are called return values. So if I dropped in this example, we see it's basically the same as our last example. But instead of printing the message in the sum function, we're returning the result out. And then the main function is actually working With that. So there's a change we had to
make in our function signature. So right here after the parameter list, and before the opening curly brace, I've listed the return values type. So in this case, I'm expecting to return an integer. So I just put it right here, inside of my function, I'm going to use the return keyword. And then I'm going to return the value of the variable that I've been building up throughout the course of the function. So in this case, I declare the result variable here, I populated in this loop. And then I return that result back now up here in
the main function, I can catch that return value by declaring a variable and setting it equal to the result of this function. So S is actually going to be an integer type, because that's what was returned out of this function. And then I can work with that integer. So if I run this, I get exactly the same behavior that I had before. But now the sum function is more of a pure function, it doesn't care what I do with that result, it's just going to generate the result and return it back To the caller. Now
another feature that go has that's actually pretty rare in a lot of languages, is the ability to return a local variable as a pointer. So in our previous example, when we return that result, go actually copied that result to another variable, and that's what got assigned. But we can also do this. So if you look here, I'm returning a pointer to an integer now. And instead of returning the result, I'm returning the address of the result. And so S is now a pointer. So I change to a dereference operation. So if I run this, it
works exactly the same way. Now again, if you're coming from another language that uses pointers a lot and doesn't abstract away the differences between working on the stack and working on the heap, then this might freak you out a little bit, because when we declare the result variable, it's actually declared on the execution stack of this function, which is just a special section of memory that's set aside for all of the operations that this function is going to be working with. So in this funk Exit, then execution Stack is destroyed, that memory is freed up.
And so in a lot of languages, this is not a safe operation, because now you're returning a pointer to a location in memory that just got freed. And so you've got no idea what value is going to be there. Well, in the go language, when it recognizes that you're returning a value that's generated on the local stack, it's automatically going to promote this variable for you to be on the shared memory in the computer, what's also called the heap memory. So you don't have to worry about this value being cleared, the runtime is going to
recognize that you're returning a pointer from the local stack, and everything is going to work for you just fine. And that makes a lot of things more convenient. Because within the function, we can work with this as a true value. So we don't have to worry about dereferencing pointers, and then just right at the end, we can return the address of the result. And the runtime makes it all work for us. Another thing that we can do in the go language, and this isn't Done very often, but there are cases where it is valuable is
using named return values. So if I drop in this example here, notice that I've changed my return value. Now I've got a set of parenthesis here. And then I've got a name for the return value and a type for it. So when you do this, this is basically syntactic sugar for declaring a result variable. So this variable is going to be available in the scope of our sum function. And then that value is going to be implicitly returned. So we can work with that result variable right here within our function. And then we don't have
to specify the name of the return variable down here in line 17, we just have to tell it to return. So when we run this, we see that once again, we get exactly the same behavior. But the body of our sum function is actually quite a bit cleaner, because we don't have to do the maintenance of instantiating, this result variable. Now, this is actually not done very often in the go language. And my suspicion is because it can be a little bit confusing to read, because your return variables Are declared way up here at the
top of the function, and your actual return is down here at the bottom. So if you're reading this code, and you're trying to figure out what this function is actually going to return, you have to come all the way back up to the function signature. So this can be a very valuable technique to use. But I would be very careful with it. Because if you've got long functions, the named result parameters can actually be more confusing instead of less confusing. So you have the option there, pick whichever one makes the most sense for your application.
The last thing that I want to talk about with return values is the fact that we can do multiple return values from a function. So in order to show you why this is valuable, let's take this example here. So I've created a simple divide function that takes in two parameters A and B, that are float 64, it's going to divide them and it's going to return that result back. So if I run this, I get 1.6 666. Like you might expect, and everything's fine. But what happens if I pass in a zero here. Now when
I run this, I get an unknown result, I get a positive infinity result. And I can't work with that in my application. So I'm going to probably cause some sort of a failure down the line. So in a lot of languages, the only thing we could do is throw an exception or panic the application and go when we detect that there's this invalid value for the parameter B. So I guess we could do that we could add some kind of logic here. If b equals equals 0.0, then we're going to panic and we're going to
say, cannot provide zero as second value. And that would work. But keep in mind when we talk about control flow and go, we don't want to panic our application as a general course of action, because panicking means the application cannot continue. Now, in fact, this application cannot continue if somebody provides the value of beat. But it's reasonable to assume that we might pass zero in for this be parameter occasionally. So instead of doing this, what we actually want to do is return an error back letting the calling function know something that they asked it to
do wasn't Able to be done properly. So instead of doing this, we're actually going to add a second return variable. So to do that, we're going to add a print here. And we're going to return an object of type error, and then close that parenthesis off. So we can return as many values as we want from a function. But this is a very idiomatic way of using the go language. So we're going to return the intention of the function as the first return value. And then we're going to return an error object in case something
went wrong. So in that case, what we're going to do is, we're going to remove this panic, because we really don't want to be doing that. And we're going to return the first value, we're just going to zero it out, because we can't do this operation, so we can't return anything meaningful. And then we're going to return an error object. Now you can generate one of those by using the air f function. And we can say cannot divide by zero. So we're going to provide a value for that error. And then that's all we need
to do here. So since we've returned To this function in the error case, then if we get past it, we can continue as if our parameters are okay. And so in that case, we can actually do our operation. And then for the error value, we're going to pass nil because no error was present. And again, this is very idiomatic go, we're going to return an error value if something went wrong, and then we're going to explicitly return nil if nothing went wrong. And then if you've read any amount of go code, you've seen this quite
a few times. We're going to check to see if that error also Got our standard if error is not equal to nil, and then we're going to put our error handling logic in here. So in this case, all we're going to do is print out the error and return from our main function. So we're going to exit our application. If we don't have that, then we're going to just print out the result of our calculation. So again, this is a very common pattern and go inside of your functions that can generate errors, you're going to
return the expected value and then an error as the second parameter, Then you're going to have a guard that's going to check for those error conditions, you're going to return as soon as possible from your function with the error value if an error is present. And the reason for that is we're going to try and left justify our code as much as possible. So we don't end up with these pyramids of doom, where we're going to have else checks. And we do all of our error checking at the bottom, we're going to do our error
checking at the beginning and then return out as soon as possible. So if we do get past that, then we're going to be on our happy path. And we're going to return out the result of a calculation and then a nil error up in the calling function, we're going to have the standard test to see if error is not equal to nil. If it isn't equal to nil, then we're going to process that error, because now we've got something we're going to have to deal with. And then again, we don't have an else block here,
we just continue moving on, we're going to make sure our error handling logic either recovers from the error Or exits onto the function. And that way, we can keep our main thread of execution left justified. So our main thread of execution here is we're going to call this divide function, and then we're going to print out the result. So any error handling should not force the main line of execution to be indented. So now if we run this, we see that I forgot to initialize my error parameter. So let's go ahead and add that. And
this is something else I should talk about. When we're receiving multiple values out of a function call, we actually have a common delimited list of those return values. So this D parameter is going to match up to this float 64. And this eerr parameter is going to match up to this error parameter here. So now if I run, everything should work. And we see that we now get an error cannot divide by zero. So our main function doesn't explode on us, we actually have something that we can work with. But if we put in a
valid value, then we're on that other path of execution, and we get the return value back out. Now, so far, we've been treating Functions as this special thing, because we're always using this func keyword, we're declaring these at the top level of our application, we're working with them. But functions and go are actually more powerful than that. Because functions themselves can be treated as types, they can be passed around as variables, they can be passed as arguments into functions, you can get them as return values, pretty much anything you can do with any other type
you can do with functions. So let's take a look at that a little bit. So in this example, I'm actually declaring a function on the fly. And this is called an anonymous function. Now we're going to continue to explore this over the next couple of minutes. But this is the simplest example I can come up with. So notice that I'm starting with the func keyword, I've got the params. For the parameters, I've got the opening and closing curly brace, but I don't have the function name here. So when you're doing this, this is what's called
an anonymous function. And this is the basic structure of a function when you're not working With functions in this traditional scope. But instead you're working with functions as types. So inside of my function body, I'm printing out the message Hello, go. And then I've got accompanying my closing curly brace, these params here. Now these params here are basically going to invoke this function. So this is an immediately invoked function, I'm defining it and executing it at exactly the same time. So when I run this, and actually does execute that function, and we get the value
Hello, go printed out. If I don't have these friends, then the compiler is a little confused. It doesn't know what to do with this function. It's just defined, but it's never used anywhere. So fails a compilation check. But if I do invoke that function immediately, then I get this behavior here. Now why would you use an anonymous function like this, I actually have no idea why you would use an anonymous function like this. I mean, there can be situations where you can declare variables inside of here. So if I declare a message variable here, and
I set that equal to this String, and then print that out, that can be valuable because you're actually generating an isolated scope. So this message variable is not going to be available in the main function is only going to be inside of this anonymous function here. Now another place that you might use this is if we've got a for loop. So if I start up a simple for loop, and I'll just count up to five, and increment by one, let's see if I can do this on the fly here. And then I come in here
and I actually print the value of i out, I'll get rid of this message here and I'll print out I, you're going to get a little bit of strange behavior. If I run this, this works, okay. But as we start getting into asynchronous code, things are going to start behaving a little bit oddly. So we do have access to this AI variable, because we're in the scope and the main function. And so inner functions can actually take advantage of variables that are in the outer scope. But the problem is, if this function is actually executing
asynchronously, then this counter variable is going to keep going. And we may Actually have odd behavior here. So the best practice is actually to provide a variable inside of here and actually pass that AI variable. And what that's going to do is we're not going to be reading from the outer scope anymore. We're going to be passing that into the function execute And that way, even if this is running asynchronously, we're going to print out the value correctly. Now, this works correctly in the playground, the way that we have this right now, because we actually
aren't doing anything asynchronously. This is all synchronous execution. And so we are safe to use this outer counter. But it's not good practice to do that. Instead, it is best practice to pass in that kind of variable. If you need that in your inner function. That way, changes in the outer scope aren't reflected on the inner scope. Now taking this a little bit farther, we can work with functions as variables, like I said before, so in this case, I've declared an anonymous function, and I've assigned it to this variable F. And then I can execute
f by just invoking it like any other function. So if I call that, we see that we do print hello, go out. So now that I've got this function defined as a variable, it's free to pass around my application. Now, you might ask yourself, what is the signature for this function. So let's go ahead and go through that. So if I get rid of the short syntax and extend this out a little bit, then I'm going to start with the var keyword. And since this is a very simple function, the type is just like this,
we have the func keyword, and then an open and close parenthesis. So the parameter is normally going here, I don't have any parameters here, I don't have any return types. So the type signature for this variable is simply func with two params. There. So if I run this, that works just fine. Now we can go a little bit more complicated. And I'll drop an example of that in just to show you how that's going to look. So in this case, I'm declaring a function signature for a divide function, that's going to take in two floats,
and it's going to return a float and an error. And you see, this is the syntax for that. So we Pass our parameter types in here. And then we have the return types in params. As well, if you have just a single return type, then you don't need these params, we could just put the type there like that. But we do have that error type this coming back. So we do need to include that. And then when I initialize that variable, I'm going to set it equal to an anonymous function that takes a and d.
And this is exactly the same divide function that we had before. And I can call that exactly like we had before. So when I run this, it has exactly the same behavior as the last example we did with the divide function. But now we have the divide function declared as a variable. And we're working with it exactly the same way as when we declared it as a function. Now, the difference between this and when we had the divide function declared globally, is if I try and call it up here and run the application, notice that
I get an error because in this case, the function divide hasn't been declared yet, because it's declared as a variable. And so I can't work with it yet. So that's Just something to be aware of. If you're going to be working with functions as variables like this, make sure that they're defined before you actually try and execute them. Okay, the last thing that I want to talk about with functions today is working with what are called methods. And there's a couple of things to talk about with those. So let me just drop in an example
that shows that. And we can walk through this and then see what it's going to do. So in this example, I've got a struct called greeter that greeter struct has two fields greeting and name. And then I've got this method on it. And we'll come back to this in a second. So in my main function, I'm declaring a greeter struct, and then I'm calling this function preceding it with the struct that I have here. And this is how we're going to do method invocation. So we call the method just like we were accessing a field,
except for we have the params here where we can pass some arguments in. Now my method declaration down here looks a lot like a function except for it's got this odd bit right here. And this is what makes this function into a method. So a method is basically just a function that's executing in unknown context, and unknown context. And go is any type. Now it's very common that we can use structs. But you can use any type. So we can make a type for an integer. So maybe we have a type for an integer called
counter. And then we can add methods on to that counter type, and work with those. So when we declare that method, we're actually going to get access to that type right here in this part. So what's going to happen when we call the greet method is the greet method is going to get a copy of the greeter object. And that's going to be given the name g in the context of this method. So then when we print out, we can access the fields on that greeter object. So we can print out the greeting and the
name. So when we go ahead and run this, we see that we get Hello go printed out. And that's the basics of a method. So methods are basically the same as functions, they just have this little bit of syntactic sugar, that's providing a Context that that function is executing in. And when we do that, we call that a method. Now when we use this syntax right here, notice that we're specifying greeter as a value type, we don't have a pointer here. So this is what's called a value receiver. The received object in this greet method
is the value greeter. So what that means is just like any other time that we're working with values, we are getting a copy of the struct, we're not actually going to get the struct itself. So if I change the value of the Name field here, and then I print the Name field out of here, so I say the new name is and then I print the Name field out, then it's no big surprise that even though I assigned an empty string to the Name field here, up here in the main function, it didn't have any
effect. Because down here in this method, we're operating on a copy of the greeter object. We're not operating on the greeter object itself. So again, that's very valuable if you want your methods to be able to access the data of their parent type without being able to manipulate Just Keep in mind there is a cost with that. So if there's greeter object was a very large struck, then we would be creating a copy of that struct every time we invoke this method. Now, as you might expect, there's another option that we have here. And that
is to pass in what's called a pointer receiver. So if we make this a pointer, and run the application, again, now we're actually able to manipulate that underlying data. So we're going to print out Hello ghosts. So the method operates in exactly the same way. And we don't have to change the format here, because we do have that implicit dereferencing of pointers that's working for us. But now when I change the value of the Name field, and print the Name field out up here, we do in fact, see that we've been able to reassign the
value of that field. Okay, so that covers working with functions in the go language. Let's go into a summary and review what we've talked about. In this video, we talked about functions and how to use them in the go language. And we started out by talking about the basic syntax of a Function, and we saw that this is about as simple of a function as we can get. So we start with the func keyword, we have a name for that function. And again, if that first letter is uppercase, then that function is going to be
published and allowed to be executed from outside of the package. But with a lowercase first letter, it's going to be kept internal to the package, then we follow with a match set of parenthesis, and then we have an open and closed curly brace. Now the open curly brace has to be on the same line as the func keyword. And the closed curly brace has to be on its own line after the final statement of the function, then we moved on to talk about parameters and parameters allow us to pass data into the function to influence
how that function executes, basically providing some variables for the function that are passed in from the outside. So we talked about how parameters are passed in as a common delimited list of the name of the parameter and the type of the parameter. So we see here we're passing into the Foo function, two parameters, The bar parameter that says type string, and the bass parameter that's of type integer parameters of the same type can be comma delimited. And the type can be listed at the end there. So in this case, we're passing in bar and Babs
as parameters. And both of those are going to be of type integer. When pointers are passed in the function contains the value in the caller. So by default, we're going to be passing in the values themselves. And so the go runtime is going to be copying that data and passing it into the function. So any changes that are made inside of the function aren't going to be reflected in the color scope. However, if you pass in a pointer, then you are going to be able to manipulate the value inside of your function. And that will
have an effect in the calling scope. So the only exception to this rule is when you're working with slices and maps, since they work with internal pointers, any changes inside of the function to the underlying data is always going to be reflected inside of The calling scope. We also talked about how you can use variadic parameters to send a list of the same types in, so it must be the last parameter in the parameter list. It's received inside of the function as a slice. And you see an example of the syntax right here. So we've
got the function foo. It has one parameter bar that's of type string, and then a parameter Baz, that's a very attic parameter of integers. So inside of this food function, we're going to have a slice called Baz. And that's going to contain all of the integers that have been passed in. Once your function finishes doing its work, a lot of times we want it to return a value back out. And in order to get that information back out, we're going to use return values. So if you have a single return value, all you need to
do is list the type. So in this case, our foo function needs to return an integer, we can also specify multiple return values. So if we're going to do that, we need to put parentheses around the types that we're going to be returning. So in this example, we're Going to be returning an integer and an error. And this is a very common pattern that you're going to see in NGO applications where we're going to have our functions return the intended value, and then an error value that's going to let the caller know if anything went
wrong. That way, the function itself doesn't have to determine whether the application needs to panic, or execution can't continue. It just knows it wasn't able to do what it was asked to do. And then it can delegate what that error means to the application to the calling function, you can also use named return values. So when you do that, instead of just providing the types in the return last, you're going to provide the name of that return value. So when you do that, the runtime is going to initialize that value for you to the zero
value for that variable. And when you use the return keyword, all you need to do is enter return on its own go is going to find the current value of those returned variables. And that's what's going to be returned out of your function. Another special behavior Of go is you can actually return the addresses and local variables as return values, and those are going to be treated properly. So when you do that those variables are automatically promoted from local memory or stack memory up into shared memory or heap memory. So you don't have to worry
about those values being cleared out as the function stack memory is reclaimed. We then started talking about anonymous functions. And we talked about a couple of different uses for those. So we have this immediately invoked function, which really isn't used too often in the go language, but it's as simple of an anonymous function as I could get. The only potential advantage that you have here is you can create an inner scope. So local variables that are created inside of this anonymous function aren't going to be available outside, but I haven't seen that very often. It's
not going to be very often that you're going need to use this kind of a function, then we also talked about how we can take that anonymous function and actually assign that to a variable. So in this example, We've got the variable a assigned to the value of that function. And then we can invoke the a function just like any other function. The only difference between this and the normal declaration of a function is that the a function can only be invoked after it's been declared. So when you declare a function using the traditional syntax,
it's actually declared at the time that the package is initialized. And so it's always available to you. When you're using this syntax, you have to make sure that a is initialized before you can call it extending on that discussion about the ability to assign functions to variables, functions, or types, just like any other type in go language, anytime you can use a primitive or a slice or an array or a map, you can use a function. So you can assign them to variables, you can use them as arguments, they can even be returned values from
functions, then we also talked about how since a function is a type, we have to have the ability to create a type signature. So if you're declaring anonymous functions, it's often most convenient just to use that Colon equals syntax and declare your anonymous function and the type is going to be inferred. However, if you're using a function as a parameter to another function, or the return value from a function, then you're going to need to specify that type signature. So we see an example here. In this case, we've got the definition of a function f.
And that function is going to take three parameters, two strings, and an integer. And then it's going to return an integer and an error type. So it's basically the same as when you're declaring a function normally, the only difference is we don't have the names for those variables. Because those names will be provided when we actually implement the function, we just need to know the types that are coming in, and the types that are coming out at this point. The last thing we talked about were methods, and how method is a special type of function
that executes in the context of a type. Now a type doesn't have to be a struct. Although that definitely is a very common use case for methods, you can actually attach a method to any custom Type. So you can create a type of an integer, and then you can add methods on to that integer. When we create a method, we're going to use a modified version of the basic function syntax. So before the name of the function, and after the func keyword, we're going to provide another set of parentheses, we're going to provide a local
name for the type that's going to receive that method. And then we're going to follow that with that method type. Now, that variable is what's called the receiver for the method. So in this case, our G variable is what's called a value receiver, which means we're going to get a copy of that greeter object. And that's going to be passed into the greet method. However, we can also use what are called pointer receivers. So by adding an asterisk in front of that greeter type, the method is going to change. So instead of passing a copy
of the greeter, we're going to get a pointer to the greeter object in there. And then any manipulations we make to the greeter object in the greet method are going to be reflected throughout your Application. Now, that's very powerful if you need the method to be able to manipulate the state of the object. It's also much more efficient if you're dealing with large structures, because instead of copying the entire structure, it only has to copy a pointer. And that's normally a much more efficient operation, I want to talk about one of the coolest features of
the go language. And that is interfaces. Now I know that interfaces are normally considered pretty humble features, and they sit in the background. It's much more fun to talk about go routines and channels, especially when you're learning to go language. But I would argue that the way interfaces are implemented in the go language are potentially one of the reasons why go applications tend to be as maintainable and scalable as they have proven to be. So we're going to start this conversation like we start every conversation by introducing the basics. So we'll learn what an interface
is and how to use them in the language itself. Then we'll move on to discuss how to compose interfaces together. Now, just like in other high level languages, such as Java, or C sharp, we can actually make interfaces of interfaces. And we'll talk about how to do that, and why that's a very good thing to do when you're writing your applications. Then we'll talk about type conversion. Now, we've touched on this a little bit before in a previous video. But when we talk about interfaces, things changed a little bit, and it's worth revisiting the topic.
Along the way, we're going to talk about the empty interface, which is a very useful general construct that we're going to deal with in our programming. We'll also revisit type switches, which we've talked about before, and we'll revisit them in the context of our interface discussion. Then we'll talk about how to implement interfaces. And there's actually two different ways that you can do that. One is by implementing with value type, and one is by implementing with a pointer. And we'll talk about some of the subtle differences that you're going to run into as you implement
interfaces with these two different types. And then finally, we're Going to talk about some best practices that have been discovered over the last few years of working with the go language about how to use interfaces in your actual production applications. Okay, so let's get started by learning the basics of using interfaces in go. So to start our discussion about interfaces, I'm going to actually build our first application up a piece at a time now often I just drop in code and talk about it. But I want to take this one step at a time so
that we're working together and understanding what's going on. So the first thing that we're going to do is we're going to introduce our first interface. So interfaces are a type, just like structs or type aliases. So we're going to start with the type keyword, then we're going to enter the name of our interface. And then the type that we're creating is a type interface. And then we're going to surround the definition of this interface with curly braces, just like we do when we're defining a struct. Now with a struct, we would add in here, the
data that we want that struct to hold on to because Structs are ultimately data containers. And so that's how we work with them. interfaces don't describe data, interfaces describe behaviors. So instead of entering a bunch of data types that we're going to be storing inside of a writer interface here, we're actually going to be storing method definitions. So I want to create a write method here. And this is actually an interface from the IO package, we're just going to be working with it here as if we created it. But this is exactly the same interface
that you would find in the IO package under the writer interface. So this method is going to accept a slice of bytes. And then it's going to return an integer and an error. Now on the writer interface, the way this works is anything that implements this interface is going to take in that slice of bytes, write it to something that something might be the console, it might be a TCP connection, it might be the file system, we don't know, we just know that we're writing a slice of bytes to something. And then the integer and
error that get returned, of course, the error Is there in case something goes wrong with the write operation. And the integer is normally the number of bytes written. So now that we have the interface defined, let's go ahead and implement it. So we're going to implement this with a console writer implementation, and that'll be a struct. And that's all we need to do with the struct definition. Now, if you come from another language, you might be looking for an implements keyword or something like that. Well, in go, we don't actually explicitly implement interfaces, we're going
to implicitly implement the interface. And we're going to do that by actually creating a method on our console writer that has the signature of a writer interface. So let me just drop that in. Because if I try and type all this out, I will screw it up, and then I'll have bugs that I have to go through. So it's much easier just to drop it in. But notice what I've done here, I've got a method on my console writer called write. So it's got the same name as my writer interface, it's accepting a slice of
bytes, and it's Returning an integer and an error. Now the implementation is whatever I want it to be. Now, in this case, all I'm going to do is convert that byte slice into a string and printed onto the console to keep things easy in the playground. But I can have my writer do whatever I want. So what's the value of doing this? Well, the value of doing this is up in my main method, I can actually create a variable that's of type writer. And let me just drop that code in and format it and set
that equal to a console writer instance. So the W variable here is holding a writer, which is something that implements the writer interface. I don't actually know the concrete type, though. So when I call the write method down here on line nine, I know how to call that because that's defined by the interface. But I don't actually know in my main function, what's being written to, that's the responsibility of the actual implementation. So I could replace this with a TCP writer, I could replace it with a file Writer, I could replace it with any other
kind of writer. And so I get what's called a polymorphic behavior. Why nine doesn't care what it's writing to, I specify that behavior before that. But then anything that's going to use this w object just knows that it can write to it. And so it can take advantage of that behavior. So if I go ahead and run this application, you see that I do get a logo printed out to the console, just like I would expect. So the key takeaway here as we're learning the basics of interfaces, is this concept of implicit implementation. And so
what that means, for example, is if you need to wrap a concrete type, and somebody hasn't published an interface, you can actually create an interface that their type implements. So we did it the other way, we created an interface, and then we created a concrete type that implemented it. But there's nothing to say we can't go the other way around. We could, for example, go to if I travel to go lang.org, and go into packages, this is actually something that I just ran into, in order to Test SQL database connections. So if I come down
to the database package, and go into the SQL package, if we look at this, notice that the DB type is a struct. So we don't have an interface here. So if our go application is talking to a SQL database, we've got concrete types all over the place. So for our transactions, we're interacting with this DB object. everything that we're doing sending SQL statements making queries are all through this concrete DB object. So how do I test that without a database? Well, the way that you test that without a database is you actually create an interface
that replicates this method signature, and the DB object from the SQL package will automatically implement it, so I don't have to worry about creating inner phases at design time if I don't need them myself, because consumers of my library or whatever I'm creating can always create interfaces later. And their interfaces can be shaped to exactly what they need for their application. Now another thing that I want to talk about before I move on here is a naming convention. Now obviously, The name of the interface should represent what that interface is going to be doing for
you. And there is one special case, if you've got single method interfaces, which are very common in the go language, then the convention is to name the interface with the method name plus er. So if we're defining an interface, like we have here with the right method, then the interface name should be writer, if we're going to create an interface with a read method on it, then the interface name should be a reader. Now if you got more than one method in the interface, things can get a little bit more challenging. But at the end
of the day, you should name your interface by what it does. And in the case of a single method interface, just add er onto the end of the method name. Okay, now, in this example, we use the struct, which is probably one of the most common ways to implement interfaces, but you don't need to any type that can have a method associated with it can implement an interface. And as we've talked about before, any type can have methods associated with It. So in order to demonstrate that, let me just drop in this example here. Now
in line 16, through 18, I've defined a new interface called incrementer. And that increment is going to be a method that only returns an integer, so it's going to increment something. So whatever we're going to implement this thing with, is going to increment values. So down here on line 20, I defined the type alias for an integer called an int counter. And then I added a method to that custom type on lines 22 through 25. And that's going to be my implementation for the incrementer interface. So the method name is called increment, and it's going
to return an integer. Now, in this case, look at what I'm doing, I'm actually incrementing. The type itself, since I've got a type alias for an integer, it's a number, so I can go ahead and increment that. And then I'm going to return it as the result of this method call. So I've actually got a type defined on an integer, and the integer itself is storing The data that the method is using. So up here in my main function, I'm going to go ahead and create that integer counter. And I have to cast an integer
to an encounter. In order to do that. That's what I'm doing here on line eight. And then I create my incrementer and assign that to a pointer of the my ns object. And we'll talk about why that has to be a pointer toward the end of this video. And then I'm just going to loop from zero to nine. And I'm going to print out the value of the increment method every time I call it. So if I go ahead and run this, I see no big surprise, I get the values one through 10 printed out.
So what's the takeaway here? Well, you don't have to use structs. To implement interfaces, you can use any kind of custom type. Now I couldn't add a method directly to the entity type. Because the event type isn't under my control that's defined in another package. It's a matter of fact, that's a primitive type, and you can't modify it. But any type that I do have control over that I can create, I can add methods to it. And if I can add Methods to it, I can implement interfaces with it. Now the next thing that I
want to talk about is how to compose interfaces together. Because this is another very powerful concept in the go language, and is one of the keys to scalability, because if you remember I mentioned a little while ago, single method interfaces are very common and very powerful in the language, because they define a very specific behavior. But by having a single method, they don't have a lot of opinions. And so they can be implemented in a lot of ways. So for example, the IO dot writer interface is one of the most common interfaces in the entire
go language, because all it does is talk about how to write two things. And we write two things all the time. So by taking as little opinion as possible, we actually make the interface very, very powerful and very, very useful. So let me go ahead and paste this example here. Because what happens if we need more than one method, but we can decompose the interfaces down. So in this case, I've created an interface that's composed Of other interfaces. So I've got my writer interface that we started the video with. And then I've added this closer
interface that just has a closed method on it, and returns an error just in case something happened when we tried to call this method. Now the writer closer interface is actually composed of the writer interface and the closer interface. And this is done exactly the same way that you do embedding with structs. We're just embedding interfaces within other interfaces. So the writer closer is going to be implemented. If an object has this method on it, and this method on it, then we can treat that as a writer closer. So as an example, I've created this
struct here, a buffered writer closer. Now, I'm not saying that this is an efficient way of doing things. This is just an example of how you might use this writer closer interface in a way that runs in the playground easily. So in this case, I've got my write method that I'm going to be implementing. And what I decided to do is I'm going to write out Whatever gets sent into the buffered writer closer. I'm going to print that out to the console in eight increments. So that's what all this code is doing, when you pass
data into the write method, it's going to store that in this internal buffer that the structure defines. And then as long as the buffer has more than eight characters, it's going to go ahead and write that out. But it won't write anything out if it's got less than eight characters. So we're basically buffering the data that we're sending in. And then down here in the close method, I've got to implement that too. And so what we're gonna do there is we're going to flush the rest of the buffer. So I'm pulling the next eight characters
out. And I'm going to write that out to the console. And keep doing that until the buffer is empty. Okay, up here in the main method, I simply create a writer closer variable, and define that using the new buffered writer closer function. And just to show you that I didn't talk about it, that's down here at the bottom, that's just a constructor function that's Returning a pointer to a buffered writer closer. And I need to do that because I need to initialize this internal buffer to a new buffer. So I have a constructor method there
just to make sure that everything has been initialized properly. So if I come back up to the main function, and look at that, then I'm going to call the right method. And I'm converting the string hello youtube listeners, this is a test over to a byte slice, because that's what the right method expects. And then I'm going to call the close method. So if I go ahead and run this, you see that I get the message printed out to the console in eight character chunks. And eventually I get all this printed out. But if I
comment out this last method, call here, you see that I don't get the a test part of the string, because that's actually a partial. And so we didn't get that full eight characters that's required for the right method to print it out. And so I didn't actually flush the buffer. Okay. So I know that maybe a little bit of a complicated example, to show a fairly simple thing. But I just wanted to show you this is how you can compose interfaces together. And as long as you implement all of the methods on the embedded interfaces,
then you actually implement the composed interface as well. The next example that I want to talk about is how we can do type conversion. So I'm going to go ahead and replace my main function here with this code here, get rid of the extra curly brace, actually, I'm going to get rid of this too, I guess I pulled in the whole function signature. And then I made a little bit of a change down here in line 13. So lines nine through 11 are our original implementation, where we're creating the new buffered writer closer or writing
a string out, and then we're calling the close method on it. But on line 13, I'm actually doing a type conversion. So using this syntax here, where I've got an object, dot, and then in parentheses, I've got a type that I'm going to try and convert this variable to. And then I can assign that to a variable such as this PwC variable right here. Now, if that succeeds, then everything's fine. And I can go ahead and work with it. Now, there's nothing useful I can do with this. But I'm just printing out the variable, because
I have to use the variables and go. So I'm just going to go ahead and print that out. So if I run this, I see I get exactly the same output I had before. But now I get the memory address of this buffered writer closer, so that tape conversion succeeded. And therefore I can work with this no longer as a writer closer, but as a buffered writer closer. So for example, if I needed to access the buffer directly, then I would be able to do that now. Whereas with the writer closer, it's not aware of
the internal fields of a specific implementation. And so I wouldn't have access to that data. Now there is a problem. However, if I import the IO package, and try and convert this to a type that it doesn't implement. So for example, if I try and convert this over to an IO reader, which is another interface, and that IO reader interface requires a read method on it. So if I try that now, let's go ahead and run that. And we see here we Put the application into a state that it can't manage. And so what it
does is whatever good go program does, when it can't figure out what to do it panics. And the panic messages interface conversion, it can't figure out how to cast a buffered writer closer into an IO reader. And so it's going to fail on us. Now, it does give us some useful information about why it couldn't do that. It says it's missing a method read. And then it's gonna give us a stack trace letting us know where that error occurred. Now, this isn't really great, because sometimes we need to try and convert an interface into something
else. And we're not sure if it's going to work or not. So it's not going to be good for our application to be panicking all the time, because then we're going to have recovers and we're going to be using that as a primary control flow concept. And we want to avoid that in the go language, because panicking is pretty expensive. So we need another way around it. Well, we just so happen have another way around it. And so I'm going to show you That. All we need to do let me rename this variable, because PwC
doesn't make sense for a reader anymore. I'm going to paste this code in. So I'm going to now try and cast it to a variable called R. I'm going to do the same type conversion, but now I'm using this comma, okay, syntax. So we've seen this before, when we were trying to pull a value out of a map and we weren't sure if it was there or not. Well, we have the same ability with type conversion. If we add a comma, okay, this is going to be a Boolean result. And then we can test against
that to see if we can work with it. So if the conversion succeeds, then we're going to get an okay value back out. If the conversion fails, then we're Going to get the zero value of whatever type we were trying to convert to. So an IO reader is an interface. And so it's zero value is going to be nil. So if we go ahead and run this now, we see that our conversion failed, but our application didn't crash. If we switch this back to a pointer to a buffered writer closer, let me go ahead and
drop that in. And we run that we got to drop out our package here that we're no longer using. And then we see that we're back to having things successfully converted. And we could work with that however we needed to. So this is really important to be aware of, especially if you're not sure if you're going to get a pointer or a value type. So for example, it would be really easy to write this and have a problem because we implemented the interface with a pointer, not with the value itself. And so we can't actually
do the conversion to the underlying value type. So let's go ahead and run this again, we see that that all works. And one more thing I want to show you let me just pull this error back up again, you'll notice that the reason this type assertion failed is because buffered writer closer does not implement writer closer now that might seem a little strange to you, because our buffered writer closer has a write method, and it has a closed method, and they have the right signature. So for some reason this works when I asked it to
convert it to a pointer, but it doesn't Work when I asked it to convert it to the underlying value. Now we'll come back in just a second and talk about why that happens. But I want to finish our discussion of type conversions first. So stay tuned, and we'll talk about why that works the way it does. So the next thing that I want to talk about is something called the empty interface. Now the empty interface is exactly that. It's an interface that has no methods on it. And we describe that using this syntax here. Now,
this isn't a special symbol, this is just an interface that we're defining on the fly, and we don't have any methods on it. So it's called the empty interface. But there's nothing special about it, we could create this as the empty interface exactly the same way by just deleting the method out. And that's an empty interface now, so you see it like this all the time. Just be aware, there's nothing special about this, it's just an interface to find on the fly that has no methods on it. Now the nice thing about the empty interface
is everything can be cast into an object that Has no methods on it even primitives because, well, an integer has no methods. And so it can be cast to the empty interface. And so this can be very useful in situations where you've got multiple things that you need to be working with. But they aren't tight compatible with one another. And you need to apply some logic later to figure out exactly what you received. But we do have a problem with the empty interface. Because we now have this my object variable that's defined as an empty
interface, we can't actually do anything with it, because my object has no methods that it exposes because it's an empty interface. So in order to do anything useful with a variable that has the type of an empty interface, you're going to need to do either type conversion, or you're going to need to start using the reflect package in order to figure out what kind of an object you're dealing with. So in this case, on line 10, I'm actually trying to type cast into a writer closer, and I'm using the comma okay syntax to see if
that worked. If it does, then go ahead and call The write and close methods like I saw before. And then I've got this other type conversion that I've done that before just to keep things consistent. So if I run this, we see that I forgot to re import the IO package, let me go ahead and pull that back in. And we see that everything works as normal. So the empty interface is very common. But just keep in mind, it's almost always going to be an intermediate step. And you're going to define a variable of the
type empty interface. And then you're gonna have to figure out exactly what you receive before you can do anything useful with it. The last thing that I want to talk about in the context of type conversions are type switches. So I want to revisit that conversation from a few videos ago. And just to show you, we can do something like this. So in line eight, I've got a variable i that's defined as the empty interface, and I'm setting it equal to the integer zero. And then I'm going to use this switch block here, and I'm
going to use this syntax. So I've got my variable name I, and then I'm going to use this dot And inside of prims, I'm going to put type. And so what this is called is this is called a type switch. So each of the cases in this type of switch are actually going to be a data type. So in this case, I'm looking to see if I've got an integer or a string, or I've got a default case, which is going to be handled by our application, just saying it doesn't know what AI is. So
let's go ahead and run this, I properly identify as an integer. So we execute this case here. If I put params around this, then of course, I is now going to contain a string. And so if I run that, again, it identifies it as a string. And if I change this, once again, maybe we can make this a Boolean, gotta spell true correctly, and run that then it has no idea what it is. So this is commonly paired with the empty interface in order to list out the types that you're expecting to receive. And then
you would add in the logic of how to process those different types. Now, I promised you that we would come back and talk about that weird type conversion behavior, where we could convert Our writer closer into a pointer to a buffered writer closer but Couldn't convert it into the value itself. So now I want to go through and have that conversation about why that happened. So let me just drop this code in. This is a much simpler implementation than what we had before. I've actually not really implemented these methods anymore, in order to keep things
as clean as possible for you to see. So all I'm doing is I'm going to create a my writer closer, and that's down here as my writer closer struct with nil implementations for the methods. But I do have the methods implemented. So I can create this object as a writer closer, and then I'm just printing out the value of the variable just so we have some use for that variable. So the go runtime will actually compile and run this, and the interface for the writer closers to find exactly the way we had before. So if
I run this, everything works out just fine. However, what happens if I change the receiver of one of these methods to a pointer? Well, if I run this, now, I get an error. And the reason that I get that Error is it can no longer convert my writer closer into a writer closer interface. And it gives us an interesting message here, it says my writer closer does not implement writer closer, the right method has a pointer receiver. And this is the key to understand what happened with this. So when we define types, and we assign
methods to them, each one of those types has what's called a method set. Now when you're working with the types directly, the method set is all of the methods regardless of the receiver types associated with that type. With interfaces, however, things change a little bit when I implement an interface with the concrete value. So notice here I'm creating my writer closer, I'm not taking the address of my writer closer, I'm using my writer closer directly. So WC is defined as holding the value my writer closer. So the method set for a value when we're talking
in the context of an interface is any method that has a value as the receiver. So the reason we're not implementing writer closer is because a write method no longer has a value receiver, It's going to point a receiver. And so its method set is incomplete. And now we can fix this by using the address of operator and running again. And notice now everything works. And the reason for that is the method set for a pointer is the sum of all of the value receiver methods, and all of the pointer receiver methods. So let's go
through that. Again, when I'm implementing an interface, if I use a value type, the methods that implement the interface have to all have value receivers. If I'm implementing the interface with a pointer, then I just have to have the methods there, regardless of the receiver type. So the method set for a value type is the set of all methods that have value receivers. But the method set for a pointer type is all of the methods with value receivers, as well as all of the methods with pointer receivers. So there's a couple of ways that we
could fix this. Now in this case, we don't need access to the underlying data. So we could just go back to a value receiver. And then this is going to work just fine. This is actually the initial Example we had. If we have one method that's going to appoint a receiver, however, we're going to need to switch that over to a pointer type. And notice I can actually remove this. And it continues to work. Or I can make both of these pointer receivers. And this continues to work as well. So this is an important concept
when you're implementing your own interfaces. If any of the methods require a pointer receiver, you're going to have to implement that interface with a pointer. If not, though, if all of the methods except value types, then you can go ahead and use a value type if that's what you want. But you could also use a pointer. Okay, the last thing that I want to talk about are some best practices when using interfaces in your own go applications. So let's take a look at those. Okay, when we're working with interfaces, there's a couple of rules and
guidelines that I'd like you to keep in mind. And these have been developed over the last few years by the NGO community, and are generally accepted as some of the best ways to use interfaces, if it's practical in your Applications. The first is prefer many small interfaces versus large monolithic ones. Now, if you need large, monolithic ones, that's fine, go ahead and compose smaller interfaces together to make those but the smaller you can make your interfaces, the more useful and powerful they're going to be. And that's not actually unique to go. No matter what language
you're working in interfaces there. They're generally having many smaller interfaces is preferable in the long run to having a few monolithic ones. Now, some examples that are in the go standard library are the IO dot writer interface, the IO dot reader interface in the empty interface. Now these are arguably three of the most powerful interfaces in the entire language. And if you think about it, writer has one method reader has one method and the empty interface has zero methods. So it's interesting support to the argument that smaller interfaces are better that some of the most
powerful interfaces in the language contain one or zero methods on them. Now when you're working with interfaces, if you're Coming from a language that has explicitly implemented interfaces, You're going to be very tempted to create interfaces and export those. So here's the guidance for that if you don't need to export the interface yourself, so if you don't have any particular reason to do it, go ahead and don't. So there are some good examples of why you would want to do that. But often, it's perfectly acceptable to export the concrete type. I'll take as an example,
the database slash SQL package that we looked at earlier in the video, where we saw that the DB object was exported as a concrete struct. And it had all sorts of methods that pointed to other concrete structs. So you can't directly mock that out for testing right out of the box. However, by not exporting an interface, it allows you as the consumer of that struct to define your own interface that you can use for testing. And the beauty of that is, if you don't use every method on the DB object, your interface doesn't have to
have every method on it, you can just expose the methods That you need, however, do export interfaces for types that you will be using. So if you're going to pull a value in, go ahead and accept an interface instead of a concrete type, if at all possible. So this is going to be almost exactly backwards from how other languages consider interfaces. And the reason is that whole idea about implicitly implementing interfaces instead of explicitly doing it. So if you were working in Java or C sharp, you could not do this, because you have to define
the interface before you implement the interface, because they're explicitly implemented. But since go has implicit implementation, you can go ahead and defer the creation of the interfaces until exactly when you need them. So if you're creating a library that other people are going to consume, you can define the interfaces that you accept. And then they can provide whatever implementations that they want. Now, if your library has reasonable defaults, then you could export those concrete types as well. But make sure that you're accepting interfaces whenever possible. And that's what This third point is talking about. design
your functions and methods to receive interfaces whenever possible. Now, that's not always possible. If you need access to the underlying data fields, then certainly taking the concrete types. But if you're accepting behavior providers, then go ahead and try and accept those as interface types instead of the concrete types. Okay, so that covers what I want to talk about with interfaces. today. Let's go into a summary and review what we've talked about. In this video, we've talked about interfaces and how to use them in the go language, we started with a discussion of the basics of
interfaces, so how to create them and how to implement them. And we ended up with code that looks something like this. So we're defining an interface as a type. So we're going to start with the type keyword, the name of the interface, and then the keyword interface. And then inside of curly braces, things are going to be a little bit different than if we were defining a struct. For example, if we were defining a struct, we would put data fields inside Of the curly braces, because we're defining the data that that structure is holding. With
interfaces. we're defining behaviors, however, so instead of adding data fields, we're gonna add method signatures. So we see here on this example, we're going to define a write method that accepts a slice of bytes, and returns an integer and an error. And then we implement that interface by creating a method on our type that has the same signature. So we don't have to explicitly state that we're implementing the interface, we implement the interface by implementing the interface by having the methods there that match the type signature for the interfaces methods, then we talked about how
to compose interfaces together, and how this is a preferable approach versus creating a single monolithic interface, if you can break that interface down into smaller types, and then compose them together. And we did that something like this. So we're going to create multiple interfaces. So we have a writer interface and a closer interface. And then when we compose them together, just Like when we compose structs, by embedding, we can embed interfaces into one another. So we can create a writer closer interface that embeds the writer interface and the closer interface. So to implement that writer
closer interface, you have to implement the right method, because it's defined by the writer interface. And you have to implement the closed method as as defined by the closer interface. So by doing this, you can actually pass smaller chunks of your interface around your application. So for example, if a method only needs a writer, it doesn't need a closer, then you can actually pass this writer closer as a writer, and it worked just fine versus passing the entire writer closer along, and potentially exposing methods to the consumer that aren't really necessary. Then we talked about
type conversion and how we can drill through the interface to get at the underlying types in case we need to work with those directly. So we had an example here, where we created a writer closure instance, and the underlying type was a pointer to a buffered writer closer, And how we could cast that back to a pointer to a buffered writer closer by using this syntax here, where we have a dot after the object and then inside of print, we put the type We want to cast to know, we learned that when we did this,
if the type assertion failed, then we're actually going to panic or application. So remember to use that comma, okay syntax, if you want to get a boolean variable out that you can run tests against to see if that type conversion succeeded. And then we talked about the empty interface and type switches. The empty interface is nothing magic, it's just an interface to find on the fly that has no methods on it. Now, it's special in that every type in go implements the empty interface. So you can store anything you want in a variable of type
empty interface. And then very often, we're going to pair that with what's called a type switch. And we see an example of that here, where we're going To use the switch keyword, we're going to have the object and then dot and params, like we do with a type assertion. But instead of having a concrete type that we're asserting against, we put the keyword type in there. And then in our case statements, we're actually going to put in the data type that we're asserting against. So in this case, we're looking for integers or strings, or we
have a default case, in case the value stored in i is neither an integer nor a string. After that, we talked about implementing with values versus pointers. And we learned about a concept called method sets. Now, when you're working with types directly, you never have to think about this because the methods are always all of the methods assigned to that type. But with interfaces, the rules change a little bit. The method set of a value is all of the methods with value receivers. So if you're going to try and implement an interface with a value
type, than all of the methods that implement that interface, have to have value receivers. With pointers, things are a little bit more flexible, Because pointers always have access to the underlying tape as well. The method sets for a pointer is all of the methods regardless of the receiver type, so all of the value receivers, as well as all of the pointer receivers. So pointer types are definitely more flexible when you're implementing interfaces. Just keep in mind, you don't want to assign pointer receivers everywhere without thinking about the idea that that gives access to the underlying
data of that type. And so that can allow methods to alter that underlying data, even if you don't want them to. So be careful when you make that choice about using pointer receivers or value receivers. The last thing that we talked about were some best practices that have evolved over the last few years about using interfaces in the go language. And we talked about use many smaller interfaces whenever possible. And then if you need larger interfaces, go ahead and compose those together with interface composition. Don't export interfaces for types that will be consumed. So if
you're creating a library, and somebody else is going to be Consuming a type, go ahead and publish that concrete type out there don't create an interface, assuming you know how people are going to use it, allow them to create the interfaces that your type will implement. That way, they don't have to implement a whole bunch of methods in their test suite that they never even use. Do export interfaces for types that you will be consuming however. So again, these two points are exactly opposite of how you're going to think about interfaces, if you're coming from
another language, such as C sharp or Java, that have explicit implementation of interfaces. So when you're defining a type that you're going to be consuming in your package, then go ahead and export interfaces. That way whoever's using your package can create their own concrete types, and implement the interfaces that you need. So you don't need to worry about the implementation, you just need to worry about the behaviors that they're exposing to you. And then, if possible, define your functions and methods to receive interfaces. Don't get too crazy with this. So don't go over the top.
Use common sense with this. But if you have the option of receiving interfaces, for example, if you don't need access to the underlying data, then go ahead and define an interface that you're going to be receiving. That way it makes your methods and functions more flexible. Since you can have multiple implementations that you never thought about at design time. And your functions and methods will continue to work. Even when those new concepts are thrown at your application, I want to have a conversation about the tools that we have available to implement concurrent and parallel programming
in the go language. Now, if you come this far in the series, or you've done any research and go at all, concurrent programming is one of the hottest topics that is talked about, especially among people who are learning to go language for the first time. So we're going to talk about this concept of a go routine, and how that enables us to create efficient and highly concurrent applications. We'll start our conversation by learning how to create go routines themselves. So this is going to be the basics of how we create go routines and how we
can work with them a little bit. Then we'll move into a conversation about synchronization. And we'll talk about two concepts, weight groups and mutexes. And how we can use those to get multiple go routines to work together. Because one of the challenges that we're going to have with go routines is also one of the greatest advantages. Go routines are going to allow our application to work on multiple things at the same time. However, you're often going to run into situations where you need a certain bit of functionality in your application to weight into one or
more of those concurrent calculations is complete. So we'll talk about how to use synchronization primitives in order to do that. Then we'll move into a discussion of parallelism. Now up to this point, our conversation is going to be about concurrency in the go language. And concurrency is just the ability of the application to work on multiple things at The same time, it doesn't mean it can work on them at the same time, it just means it has multiple things that it can be doing. When we talk about parallelism, we'll talk about how we can take
our NGO applications and enable them to work on those concurrent calculations in parallel, or in other words, introduce parallelism into our applications. And finally, we're gonna wrap this video up again, with a little section on best practices, just to talk about some of the gotchas that you can run into with concurrent and parallel programming, and some of the tools that are available to help keep your application safe and away from those minefields. Okay, so let's get started by talking about how to create go routines. Okay, so the first thing that you're going to notice is
that we're in Visual Studio code right now. Now, the reason for that is while we can certainly play with go routines in the playground, when we start to get into parallelism, that's going to be limited by the playground, because the playground only enables us to use one core at a time. So when we're running locally, we can use as many cores as we want. So we can truly run our applications in parallel. So some of the things that I want to show you are going to be easier to illustrate in this environment. So the first
thing that I want to show you is how we can create our very first go routine. So the first thing that we're going to need to do is we're going to need to have a function here. So I will create a function called Say hello. And this is going to be a very simple function, all it's going to do is well say hello. So we'll start with that. And that's going to be just enough for us to get started seeing what's going to happen with our application. So we can of course, call the say hello
function and call that from the main function. So we can run this application by just using go run and pointing it to that file. And of course, it says hello, so no big surprises there. Now, to turn this into a go routine, all we have to do is in front of the function invocation, just type the keyword go. Now what that's going to do is that's going to tell go to Spin off what's called a green thread, and run the say hello function in that green thread. Now I need to take a little bit of
a moment here to talk about threads. most programming languages that you've probably heard of and worked with us, oh s threads are used operating system threads. And what that means is that they've got an individual function call stack dedicated to the execution of whatever code is handed to that thread. Now, traditionally, these tend to be very, very large. They have, for example, about one megabyte of RAM, they take quite a bit of time for the application to set up. And so you want to be very conservative about how you use your threads. And that's where
you get into concepts of thread pooling and things like that, because the creation and destruction of threads is very expensive. And so we want to avoid that in most programming languages, such as Java, or C sharp. Now, in go, it follows a little bit of a different model. And as a matter of fact, the first place I saw this model was used by the Erlang language. And this is using what's called Green threads. So instead of creating these very massive heavy overhead threads, we're going to create an abstraction of a thread that we're going to
call a go routine. Now, inside of the go runtime, we've got a scheduler that's going to map these go routines onto these operating system threads for periods of time, and the scheduler will then take turns with every CPU thread that's available and assign the different go routines, a certain amount of processing time on those threads. But we don't have to interact with those low level threads directly. we're interacting with these high level go routines. Now the advantage of that is since we have this abstraction go routines can start with very, very small stack spaces, because
they can be reallocated very, very quickly. And so they're very cheap to create and to destroy. So it's not uncommon in a go application to see 1000s or 10s of 1000s of go routines running at the same time. And the application is no problem with that at all. Now, if you compare that to other languages that rely on operating system threads that have one megabyte of overhead, there's no way you're going to run 10,000 threads in an environment like that. So by using go routines, we get this nice lightweight abstraction over a thread, and we
no longer have to be afraid of creating and destroying them. So anyway, let's go ahead and run this and see what happens. And it's going to be a little disappointing because you notice that our message doesn't print out. And the reason for that is our main function is actually executing in a go routine itself. So what we did here in line six was we told the main function to spawn another go routine, but the application exits as soon as the main function is done. So as soon as it spawn that go routine, it finished, it
didn't have any more work to do. So the say hello function never actually had any time available to it to print out its message. So we can get around that a little bit by using a horrible practice, but it's good enough to get us started in understanding this. So We'll just put an arbitrary sleep calling here in order to get the main function to delay a little bit Now when we run the application, we see that we do get our Hello message printed out. Now, as opposed to our first run of this, it's not actually
the main function that's executing this code. It's a go routine that we're spawning off from the main function. And that's what's responsible for printing out the message. Okay, now, this is a pretty typical use case of go routine where we're using the go routine to invoke a function. But we don't have to do that. As a matter of fact, let me just drop in this example here, which is basically the same, except for instead of using a named function, I'm using this anonymous function here. So notice that I've got this anonymously declared function, and I'm
invoking it immediately. And I'm launching it with go routine. Now, what's interesting about it is I'm printing out the message variable that I've defined up here on line nine, down here inside of the go routine. So if I run this, we do in fact, see that it works. Now The reason that it works is go has the concept of closures, which means that this anonymous function actually does have access to the variables in the outer scope. So it can take advantage of this MSG variable that we declared up here on line nine, and use it
inside of the go routine. Even though the go routine is running with a completely different execution stack. The go runtime understands where to get that MSG variable, and it takes care of it for us. Now, the problem with this is that we've actually created a dependency between the variable in the main function and the variable in the go routine. So to illustrate how that can be a problem. Let me modify the example just a little bit. So I'm declaring the variable message and setting it equal to Hello, and then printing it out in the go
routine. And then right after I launched the go routine, right here on line 13, I'm reassigning the variable to goodbye. So if I go ahead and run this, you'll see that we in fact, get goodbye printed out in the go routine, not Hello, like you might expect Based on how the program is written. And the reason for that. And it's not always going to be guaranteed to execute this way. But most of the time, the ghost scheduler is not going to interrupt the main thread until it hits this sleep call on line 14. Which means
even though it launches another go routine on line 10, it doesn't actually give it any love yet, it's still executing the main function. And so it actually gets to line 13 and reassigns, the value of the message variable before the go routine has a chance to print it out. And this is actually creating what's called a race condition. And we'll come back and talk about race conditions at the end of this video. But this is a bad thing. And generally, it's something that you want to avoid, so that you can access variables via the closure,
it's generally not a good idea to do that. So if that's not a good idea, what are your other options? Well, notice that we have a function here. And this is just a function invocation, there's nothing special about it, just because we put the Go keyword in front of it, it's just a function. So functions can take arguments. So what happens if we add a message argument here, and then down in the prints, where we're actually invoking the function? What if we pass in the message parameter? Well, since we're passing this in by value, so
we're actually going to copy the string hello into the function, then we've actually decoupled the message variable in the main function from the go routine, because now, this message that's going to print out is actually a copy that we're passing in when we're invoking the function for the go routine. So now if we run this, we see that we get Hello printed out. So this is generally the way that you're going to want to pass data into your go routines, use arguments to do that, and really intend for the variables to be coupled together. Now,
this example so far is working. But it's really not best practice. And the reason it's not best practice is because we're using this sleep call. So we're actually binding the applications performance and the applications clock cycles to the real World clock. And that's very unreliable. So in order to get your applications to work, you're typically going to have to sleep for a very long time relative to the average performance time in order to get the performance to be reliable. So we don't want to use sleep calls in production, at least not for something like this.
So what are the other alternatives? Well, one of the other alternatives that we have is to use what's called a weight group. So let's go ahead and add one in. And then while we're doing that, we'll talk about what they are. So I'm going to create another variable. And it looks like my auto formatting just helped me here. And we'll pull that from the sync package. And we'll create an object of type weight group. So I just need to put my curly braces here to initialize it. Now what a weight group does is it's designed
to synchronize multiple go routines together. So in this application, we've got two go routines that we care about, we've got the go routine that's executing the main function. And we've got this go routine that we're spawning off here On line 13. So what we want to do is we want to synchronize the main function to this anonymous go routine. So we're going to do that by telling the weight group that we've got another go routine that we wanted to synchronize to it starts off synchronizing to zero. And so we're going to add one because we
want to tell it that we're going to synchronize to this go Right here. Now once it's done, we don't need this line anymore. Once it's done, we're going to go ahead and exit the application. And we will do that by just waiting on the weight group. And we do that by using the weight method right here. Now when the go routine is done, then it can tell the weight group that it's actually completed its execution. And we do that by using the done method. So if we execute that, basically, what that's going to do is
it's going to decrement, the number of groups that the weight group is waiting on. And since we added one, and it's going to decrement by one to be down to zero, and then the weight method will say, Okay, it's time for us to go ahead and finish up Our application run. So if I save this off, and I go ahead and run it, we see in fact that our application is performing as it did before. But now it's taking just enough time to complete the execution. We're not relying on the rolling clock anymore, and having
to Jimmy run with variables and hope that everything stays consistent. Now, in this example, we're just synchronizing to go routines together. But only one of the go routines is really doing any work. The main function in this example is just storing data, and spawning other go routines. But we can have multiple go routines that are working on the same data. And we might need to synchronize those together. And that can be a little bit tricky. So let me drop in this example here, and we'll talk about it. So I'm creating weight group again, up here
on line eight. And then I'm initializing a counter variable. inside of my main function, I'm actually spawning 20 go routines, because inside of this for loop, each time I run through, I add two to the weight group to let it know there are two more go routines that are running. And then I spawn a say hello, and then I spawn an increment here. And then I just have a wait method call here on line 17, just to make sure that the main function doesn't exit out too early now and say hello, all I'm going to
do is I'm going to print out Hello, and I'm going to print out the counter value. And then in the increment function down here, I'm just going to increment the counter by one. Now after each one of those is done, I'm going to call the done method on the weight group. And everything should be just fine. Now notice that I've broken my own rule here, the weight group is actually being accessed globally in this application. And that makes sense, because I actually do want to share this object, and the weight group is safe to use
concurrently like this. It's designed to be using this way. So let's go ahead and run this application and see what's going to happen. So our intuition says that we're going to print say, Hello. So it should print say hello, zero, because the counters Value is zero right here. And then it's going to increment it. And then it's going to say hello, again, it's going to increment it. So should say hello, number zero, hello, number one, hello, number two, and so on. So let's go ahead and run this and see what happens. And we see that
we get a mess, we in fact, don't have any kind of reliable behavior going on here, we printed one twice, and then 2345. So that seemed to work consistently. And then we jumped all the way to nine, we printed 10 out twice, and then we went back to nine for some reason. And if we run this again, we'll get a completely different result. So what's happening here is our go routines are actually racing against each other. So we have no synchronization between the go routines, they're just going hell bent for leather, and going as fast
as they can to accomplish the work that we've asked them to do, regardless of what else is going on in the application. So in order to correct this, we need to find a way to synchronize these guys together. Now, we could probably find a way to use a Weight group on this. But we've already talked about weight groups. So I want to talk about another way to do this. So we're going to introduce this concept of a mutex. So with a mutex. let me paste this example in here, and then we'll talk about what it
does. But a mutex is basically a lock that the application is going to honor. Now in this case, you see on line 11, I'm creating what's called an rW mutex, which is a read write mutex. Now a simple mutex is simply locked or unlocked. So if the mutex is locked, and something tries to manipulate that value, it has to wait until the mutex is unlocked. And they can obtain the mutex lock itself. So what we can do with that is we can actually protect parts of our code so that only one entity can be manipulating
that code at a time. And typically what we're going to use that for is to protect data to ensure that only one thing can access the data at a single time. With an rW mutex, we've changed things a little bit. We've basically said as many things as want to can read this Data, but only one can write it at a time. And if anything is reading, then we can't write to it at all. So we can have an infinite number of readers, but only one writer. And so when something comes in and makes a write
request, it's going to wait till all the readers are done. And then the writer is going to lock the mutex. So nothing can read it or write it until the writer is done. So in this modification, actually, I don't want to talk about that line yet. We'll come back and revisit that. So in this modification, what I've done here is I'm attempting to use a mutex to synchronize things together. So the modification is down here in my say, Hello, I'm just reading the value of the counter variable. And that's what I'm trying to protect. I'm
trying to protect the counter variable from concurrent reading and writing because that's what was getting us into trouble. So on line 22, I obtained a read lock on the mutex and then I print out my message and And then I released that lock using the R unlock method. Now in the increment, that's where I'm actually mutating The data. So I need to write lock. And so I'm going to call the lock method on the mutex, increment the value. And then I'm going to call unlock. Now, if I run this application, I actually haven't gotten quite
where I want to be. So I don't get the weird random behavior that I was seeing before. But you notice that something seems to be out of sync still, because I get Hello, one, hello, two, and then it stays at two. And if I keep running, this actually can get different behaviors. But notice that I'm always going in the proper order. So I fixed part of my problem, but I haven't fixed all of it yet. So I can keep running. Actually, that one got pretty close. But there's obviously something else going on here. With the
reason that we have an issue here is we're still working within the go routines. So if this say hello, function gets executed twice by its go routines, and the increment function doesn't get called in between. That's where we get this behavior here, where we actually get the same message printing out twice, because we don't have a chance To lock this mutex before we try and read it that second time. So the way to address this is we actually have to lock the mutex outside of the context of the go routine. So we have a reliable
execution model. So let's go ahead and paste in a small modification here. Now all I've done is I've moved the locks out here. So the locks are now executing before each go routine executes. And then I unlock them when the go routine is done. So if I run this, we actually see that I now get the behavior that I expect, I see zero through nine printed out. And if I run it again, and I run it again, and run it again, everything is working great. So the reason that this is working is I'm actually locking
the mutex is in a single context. So the main function is actually executing the locks. And then asynchronously, I'll unlock them once I'm done with the asynchronous operation. Now the problem with this application is I basically have completely destroyed concurrency and parallelism in this application. Because all of these mutexes are forcing the data To be synchronized and run in a single threaded way. So any potential benefits that I would get from the go routines are actually gone. As a matter of fact, this application probably performs worse than one without go routines, because I'm mucking around
with this mutex. And I'm constantly locking it and unlocking it. So this is an example where if this is all that this application needed to do, we would actually be much better served by removing the go routines, and just running this with a single execution path and removing any concurrency at all. However, there are often situations where you can get a significant advantage by running things in parallel. And so you can use weight groups, or mutex is in order to synchronize things together, and make sure that your data is protected, and everything is playing well
together. Now I have this line in here, and I apologize for that I really shouldn't have had that in these earlier examples. But I do want to talk about this function from the runtime package called go max procs. So in modern versions of go, if you look at This go max procs variable, let's just go ahead and execute this simple program, all it's going to do is it's going to tell me the number of threads that are available. So it prints out that there are four threads available in the application ends. Matter of fact, let
me just add this carriage return in here and run this again, that way things look a little better. And you see that I have four threads available. So by default, what go is going to do is it's going to give you the number of operating system threads equal to the number of cores that are available on the machine. So in this virtual machine, I've exposed four cores to the VM. So I have by default four oh s threads that I can work with. Now, I can change that value to anything I want. So for example,
I can change that to one. And now my application is running single threaded. So now I have a truly concurrent application with no parallelism At all. So this can be useful in situations where there's a lot of data synchronization going on. And you really need to be careful to avoid any kind of race conditions that parallelism can incur. And maybe there's no better way to do it. Now, I would say there's an architecture problem there. But it is possible to run an application in a single threaded way, by setting go max procs equal to one.
Now if you're wondering what this negative one does, when you invoke the go max procs function, it actually returns the number of threads that were previously set. And if you pass a negative number, then it doesn't change the values. So this go max procs, negative one, all that's doing is that's letting us interrogate how many threads we have available. Now we can also set this to for example, 100. There's nothing stopping us from creating a massive number of operating system threads. Now what I found in working with NGO is that go max procs is a
tuning variable for you to work with. So the general advice is one operating system thread per core is a minimum. But a lot of times you'll actually find that your application will get faster by increasing go max procs beyond that value. Now if you get up too high Like, for example, 100, then you can run into other problems, because now you've got additional memory overhead. Because you're maintaining 100 operating system threads, your scheduler has to work harder because it's got all these different threads to manage. And so eventually, the performance peaks and it starts to
fall back off, because your application is constantly rescheduling go routines on different threads. And so you're losing time every time that occurs. So as you get your application closer to production, I would encourage you definitely develop with go max procs greater than one because you want to reveal those race conditions as early as possible. But just before you release to production, you might want to run your application through a performance test suite with varying values of go max procs, to see where it's going to perform the best. Now, the last thing that I want to
talk about are some best practices To keep in mind when you're working with go routines in the go language. So let's take a look at those next. Go routines in the go language are very powerful, and it can be easy to let them get a little bit out of hand. So I want to go through and give you some advice on how to work with go routines in your own applications. The first bit of advice is, if you're working in a library, be very, very careful about creating go routines yourself, because generally, it's better to
let the consumer control the concurrency of the library, not the library itself. If you force your library to work concurrently, then that can actually cause your consumers to have more problems synchronizing data together. So in general, keep things simple, keep things single threaded, and let the consumer of your library decide when to use a go routine and when not to now, this advice can be softened a little bit, if you have a function called that's going to return a channel that will return the result, then having the go routine in there might not be such
a bad thing, because Your consumer never really has to worry about how that unit of work is getting done. They don't really care if it's running concurrently or not. Because they are just going to be listening for the result on a channel. But we haven't talked about channels yet. So we'll revisit that topic in the next video. But for now, if you're creating a library, trying to avoid go routines, at least go routines that are going to be surface to the consumer and have them forced to work with them. When you create a go routine,
know how it's going to end. Now we're gonna see how to do this a little bit more when we talk about channels. But it's really easy to have a go routine launched as kind of a watch or go routine. So it's going to be just sitting out there listening for messages to come in. And it's going to process those messages as they arrive. However, if you don't have a way to stop that go routine, that go routine is going to continue on for ever. And so it's constantly going to be a drain on the resources
of your application. And eventually, as the go routine ages, it Could even cause other issues and cause your application to crash. The other thing that I want to give you some advice about is check for race conditions at compile time. So I want to jump back over to the editor and show you how to do that. But it's very important and very simple to do in most environments that go runs in. So let's jump over to the editor and take a look at that in order to see if we don't have to go any further
than this example. Now I know this is right from the beginning of the video, and it's got sleeps in there. And it's got some bad practices. But if you remember, if we run this application, then it prints goodbye instead of the Hello message that we originally printed. So how could we have detected this without running the application? Well, you might not think it's terribly important to be able to do that. Because it's obvious, we've got some kind of a problem here. And all we have to do is apply our debugging skills. But there are other
cases where this is very, very subtle and very, very hard to track down without A little bit of help. Well, fortunately, the go compiler has quite a bit of help available to you. And it's as simple to invoke as just adding a dash race flag to go run, go install, go build whatever you're using to get your application up and running. So let's go ahead and try that and see what it says about our little application here. And you notice it does run the application because we invoked go run so we see goodbye printed here.
But notice what we got up here we got this data race message. So it's telling us it sees that the same area of data is being accessed by two different executing go routines. So it says the first one that it found was in go routine six which an internal identifier unless we're profiling, we've got no idea what goroutine six is, but it does tell us it was invoked on line 11. So apparently in this run, go routine six was this go routine right here. And it was accessing the MSG variable. It also sees that we
access the MSG variable on line 13, which is in our main function. And so by adding the dash race flag, we get All of this additional information where the go compiler itself, analyzes our application and tries to determine if we have any race conditions. So I would strongly encourage you if you get any kind of concurrency at all in your application, you're going to want to run this because it's very simple check it runs very, very quickly. And it's going to help you prevent very subtle bugs from getting into your production code. Okay, so that's
what I have to talk about. With go routines, maybe you are expecting more. But go routines are really quite simple. Now when we get into our next conversation, which will be about channels, things get a little bit more complicated, but go routines are relatively straightforward. So let's go into a summary and review what we've talked about in this video. In this video, we learned about go routines and how we can use them to create concurrent and parallel execution paths in our applications. We started by learning how to create go routines themselves. And we learned that
by adding the go keyword in Front of a function call, we've created a go routine. And that's all that it takes. There's no special semantics, there's no special things that need to be done. It's simply a function call with the keyword go in front of it. Now when we're using anonymous functions, we in general want to pass data as local variables. So you want to add a parameter into that anonymous function and pass data into the go routine, instead of relying on the closures to prevent any kind of race conditions as you try and manipulate
that data. And that's not always true. We saw with weight groups that we access that globally, because that was our intention, we truly did want that to be available in multiple scopes. But even then we could pass a pointer in, in order to be very clear about what information that go routine should have access to. Then we talked about the different ways that we can synchronize multiple go routines together, one of the challenges that we have with go routines is now we've got all sorts of things happening. And there's no way to ensure without synchronization,
how they're Going to interact with one another. Now for a lot of concurrent calculations, that's not a problem at all, because the two might not be related to one another. But often you get into situations where you're relying on the result of multiple calculations, or something needs to know the result of the work that's been done, or you've got a shared memory issue. And you need to make sure that those go routines aren't trying to manipulate the same data at the same time. So we can use weight groups to wait for groups of go routines
to complete. So we saw that we have three methods that are interesting. There, we have the Add method to inform the weight group that there are more go routines for it to wait on, we have the weight method that's going to block the go routine that is called on until the weight group is completed. And then we have the done method available on that weight group that lets the weight group know that one of the go routines is completed with its work. We also talked about the mutex and the rW mutex, and how those are
generally used to Protect data. So if you have a piece of data that's going to be accessed in multiple locations in your application, then you can protect that access by using a mutex or an rW mutex. To ensure that only one go routine is manipulating that data at one time. We then talked about parallelism and how parallelism can introduce some really tricky challenges into your go applications. We talked about how by default go will use the number of CPU threads equal to the number of available cores on the computer that is running on. We talked
about how we can change that by using the go max procs function from the runtime package. And we talked about how more threads will generally increase performance. But too many can actually slow it down. So in general, if you're developing an application, you want to start from day one with go max procs greater than one to find any concurrency and any race conditions early on in your application development. But don't settle on a final number until right as you get close to production and you have a performance test suite that you can work With. To
find out what the best value for go max procs is for your application. Because while the starting number is a very good number to start with, a lot of applications actually perform better with a value higher or lower than that default value. And finally, we wrapped up with a discussion of some best practices to keep in mind when you're working with go routines. We learned that if you're a library developer, you should avoid the creation of go routines that are going to be exposed to the consumer of your library. Let the consumer control the concurrency
of your application because they're the ones that are in the best place to know if something needs to be executed single threaded, or if it can be executed concurrently. When creating a go routine, know how it's going to end. It's very easy to get into situations where go routines start leaking memory because they're never cleaned up because they never quite get done with their work. Now normally a go routine is killed as soon as it finishes its execution. And we saw that with the main function, the main Function runs in a go routine. And that
go routine terminates as soon as the main function exits. We also saw in our say hello function, as soon as it printed its message out and the function exited. That go routine was killed and it was cleaned out. So it was very clear when those go routines life cycle is going to be over. However, if you've got go routines that are listening for messages in a continuous loop, then make sure that you code in a way to shut those go routines down so that once you're done using them and clean up the memory that they're
using. Also, as you're going along with your application development, check for race conditions, it's not that hard to do. You just have to add dash race onto the go command that's compiling your application. And then the go compiler is going to analyze your application and try and locate places in it that have the potential of access Seeing the same memory at the same time, or in an unsynchronized way, causing very subtle and potentially very disastrous bugs where your application when it gets to production. Over The course of this video series, we've talked about a lot
of structures and techniques and 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 go 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 in 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
1000s or even 10s of 1000s of things to be going on in your application at the same time. On 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 we'll 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 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 same rate. Then we'll talk about how we can close channels once we're done with them. We'll then revisit the topic of four 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 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 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 on the simplest form of the make function with working with channels, we're going to use the channel 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 could 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 wait group. And then we'll go ahead and create those go routines, and then we'll talk about those. So let me just drop in the rest of the code here. And you can see my first go routine is an anonymous function actually both of the marm 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 less than 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 the 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 dash, 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 can 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 10 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 out really well, 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 them asymmetrical, things won't work very well. So one of the things you might want to do to play with this example is take this go routine, and move it outside of the for loop. So you're gonna have one receiver and multiple senders 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 then calls 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 printed 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 and this go routine, which is then printing the message out. So both of these go routines are actually 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 is 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 the 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 runtime 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 deadlock, 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 is nice deal for solving that problem. But I will
talk about the problem that it is solving. And that is by using buffered channels. So if 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 received 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 buffer 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 column 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 burst happens. And it's going to have to
have a way to deal with it, one of 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 if 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 I instead of processing the message once and Then having this first go routine exit, I'm actually going to use a for range loop. But notice what I'm arranging 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 had, for example, a second variable here would be the value. Well, when you're arranging 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 deadlocked this go routine right here, and
everything died. We're in our new application, we're actually deadlocking 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 for 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 can 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 close 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 four
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, we're 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 weight group. And then we exit the go routine, all of our go routines exit properly, and
we have no more deadlocks. 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 close 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 channel is 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 go 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 if 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 talked 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 the message into a closed channel. And we can't detect if a channel 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 four 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 change this example up a little bit, and this is going to do exactly the same thing as our current example, here using a four range loop, but instead of using the four range loop, and having go automatically processed 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 and 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 channel is open,
then okay is going to be true. If the channel is closed, then Okay, it's 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 channels closed, and I'm not going to be receiving any more messages from it. So this is functionally exactly the same as the four 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 for range construct. But there may be situations where you're receiving data from a channel, and you're 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 what you see here is I've got some constants that are declaring my log level, I've got a struct that I've declared the holding the timestamp for the login tree,
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 idea is 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 four 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 note 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 shifted over to Visual Studio code, you will actually get real timestamps. 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 closed 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 has 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 can leak enough resources that it can 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 use 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 kinds 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 constants have 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 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 sent. So this is pretty common, you might be tempted if you're new to the language, like I first did 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 struct 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 a 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 logger, 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 logger function, we've got an infinite loop now, and we're using this select block. So what this SELECT statement does is the entire statement is going to block until 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 dumb
channel. And that is going to be an empty struct. And I'm just going to define that empty struct on the fly here. So this is a little bit confusing syntax. But this is the type signature for my struct. So I'm defining a struct with no fields. And then I'm initializing that struct 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 handles be able to terminate. So very often you're going to send in normally as a parameter, you're going to send in this done channel. And then whatever's 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 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 a default block. So this is useful. If you want to have a non blocking SELECT statement, then you need to have that 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 routine to make a channel. When we do
make those channels, those channels are strongly typed. So we're going to use the chain 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 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 went 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 have multiple senders and receivers. As a matter
of fact, it's very common. As a matter of fact, it's 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 10 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. Buying 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 chain 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 chain keyword, and we can make a receive only channel by adding the arrow before it. We then talk 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 a 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 a second argument to the main function. And that's going to allow the channel to have an internal buffer to decouple
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 asymmetric 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 four range loops, and specifically how to work with them with channels. And we learned 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 four 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 how we can use for range loops to monitor channel and process messages as they arrive. So the four 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 four 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 kinda like switch statements, but they work only in the context of channels, 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 any 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
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 block where the first one that 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
for now. This is Mike vansickle wishing you luck in all of your gopher endeavors. Take care