- Interfaces are keys features in Go
- We know that:
- Every value has a type
- Every function has to specify the types of its arguments
- So should every function accomodate different types even if the logic in it is identical?
- Let's imagine we have the following structs
type englishBot struct{}
type spanishBot struct{}- They are both bots so they are very similar to each other
- The following functions would probably be different for each bot
- NOTE: For receiver functions, when the instance variable is not being used in the function, we can remove it
func (englishBot) getGreeting() string {
// Imagine some logic that is custom to englishBot
// Custom implementation for englishBot
return "Hello!"
}
func (spanishBot) getGreeting() string {
// Imagine some logic that is custom to spanishBot
// Custom implementation for spanishBot
return "Hola!"
}- However, the functionalities in the following functions are not specifically tied to a bot type
func printGreeting(eb englishBot) {
fmt.Println(eb.getGreeting())
}
func printGreeting(sb spanishhBot) {
fmt.Println(sb.getGreeting())
}- We could generalize them with any other types of things that are similar
- In part, this is what Interfaces resolve: Makes it easier to re-use codes in our codebase based on type generalization
- We can refactor those to make use of interface
- Interface is like a contract with a type
- As long as a
typeimplements the same functions defined by the interface, they are good to go - Format of Interface Declaration
- As long as a
type <type_name> interface {
<func_name>(<list_arg_types>) <return_type>
<func_name>(<list_arg_types>) <return_type>
...
}- Interfaces can define shared functions that must be defined by implementing types
type IBot interface {
getGreeting() string
getBotVersion() float64
respondToUser(user) (string, error)
}
type englishBot struct{}
type spanishBot struct{}
// Interface can define shared functions
func printGreeting(b IBot) {
fmt.Println(b.getGreeting())
}- Interfaces are essentially
typedefinitions - Interfaces define what functionalities a
typeshould implement- It is a contract with a
type - A
typethat wants to act asIBotmust implement the functions defined by theIBotinterface - Interfaces allows our code to be more DRY
- It is a contract with a
- Interfaces are NOT Concrete Types
- Concrete Type
- We can create a value from directly
- E.g.
engishBot,spanishBot,int,string...
- Interface Type
- We cannot create values directly from an Interface
- E.g.
IBot - Interface Types are only used to define arguments taken by functions
- Concrete Type
- Interfaces are NOT Generic Types
- Go does not have support for Generic Types
- Interfaces are Implicit
- We do not explicitly specify that a type implements an interface
- As long as the concrete types follow the specified contract functions, they are a honorary members of the interface
- In Go, we do not use
implementkeyword with interfaces - Might make it a bit confusing which types implement an interface
- Interfaces are a Contracts to help us manage types
- But we stil need to know how to implement the logic well
- If we don't, we will have GIGO (Garbage-In, Garbage-Out)
- This is an example of using Interface in Go
- We will use the
net/httppackage to make HTTP requests
import (
"fmt"
"net/http"
)
func main() {
// Create an HTTP request
resp, err := http.Get("https://example.com")
// Error Handling
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
// Log out the response
fmt.Println(resp)
}- But the response we get is not actually the HTML representation
- The response
respis a pointer
- The response
// http.Get()
func Get(url string) (resp *Response, err error) {...}respis actually astructthat contains information about the response object (docs)resp.Bodyis of typeio.ReadCloserio.ReadCloseris an interface
type ReadCloser interface {
Reader
Closer
}
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}So overall, we have:
Response Struct {
Status string
StatusCode int
Body ReadCloser interface {
Reader interface {
Read(p []byte) (n int, err error)
}
Closer interface {
Close() error
}
}
}- We can use interface as a type inside of a struct
- It means that the field can have any type that can fulfill the contract of the specified interface
- E.g.
ReadClosercan take either aReaderor aCloser
- In Go, we can take multiple interfaces and assemble them together to create another interface
- Meaning that all the contracts of all specified interfaces must be satisfied
type ReadCloser interface {
Reader // This is an interface
Closer // This is an interface
}- One of the most common interfaces in Go
- It is possible for a program to read from different kinds of data sources (e.g. Text Files, HTTP Request Body, Image, User Inputs...)
- Each one of those handler functions might return different data types
- Each one of those handler functions might have different custom implementations
- Without interfaces, we would have to define handler functions for each type
- Though those handler functions would have the same logic
- The solution to make this more DRY is the
Readerinterface
[Different Src Types] --> [Reader] --> Universal Data Format- We can think of the interface as an adapter to generalize
Readerrequires to define a function that can output a[]byte- We can write any different types of functions that can do so
func Read(p []byte) (n int, err error) {...}- The
Readerinterface requires the implementation of aRead()function- Takes the original raw data and feed it into the
[]byte []byteis passed toReadfrom the thing that wants to consume the data- Remember that a
[]byteis passed by reference- Modifying it means modifying the same object in memory
- Takes advantage of the concept of pointers
n intis the number of bytes that was read into that sliceerr errorwhen something goes wrong
- Takes the original raw data and feed it into the
So our main function becomes like this:
func main() {
// Create an HTTP request
resp, err := http.Get("https://example.com")
// Error Handling
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
// A byte slice "pointer" for getting the http data
// make(<type>, <number_of_elements>)
// Initialize with a large number to fit what Read() will pass to it
bs := make([]byte, 99999)
// Pass the byte slice to the Read function
// Read() does not automatically resize the slice
// Only read data into the byte slice until it is fully
resp.Body.Read(bs)
// Print out the actual byte slice
fmt.Println(string(bs))
}- Up to now, we have had the following diagram
- Working with the
Readerinterface
[Different Src Types] --> [Reader] --> [Universal Data Format: []byte]- Go has another interface that can do the exact opposite:
Writerinterface- Describes something that can take info and send it outside of the program
- Requires to implement a
Write()function
[Universal Data Format: []byte] --> [Writer] --> [Some form of Output]- So we need to find something that implements the
Writerinterface- Use that to log out the data that we received from the
Reader
- Use that to log out the data that we received from the
io.Copy()implements theWriterinterface- It requires something that implements the
Writerinterface - It requires something that implements the
Readerinterface - Takes some information from a source and copy it to some destination location
- It requires something that implements the
Copy(Writer, Reader) (int64, error)- Here, we will only use the Standard Output (console) for now
os.Stdoutimplements theWriterinterface- Actually of type
*File, which implements theWriterinterface *Filehas a functionWrite()
- Actually of type
resp.Bodyimplements theReaderinterface
func main() {
// Create an HTTP request
resp, err := http.Get("https://example.com")
// Error Handling
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
// Take the []byte from the resp.Body that is a Reader and output to the standard output
io.Copy(os.Stdout, resp.Body)
}- Takes something that implements the
Writerinterface (i.e. has aWrite()function) for writing into:os.Stdout - Takes something that implements the
Readerinterface (i.e. has aRead()function) to read data from:resp.Body - Pass them to
copyBuffer()- This handles the creation of the byte slice and the internal logic of passing the byte slice to the
Read()function
- This handles the creation of the byte slice and the internal logic of passing the byte slice to the
- Here is an example of a custom type that implements the
Writerinterface - A
Writerimplementation must define aWrite()function
type Writer interface {
Write([]byte) (int, error)
}- So we have to create a type that would fulfill this condition in order to create a
Writer
// Custom Interface
// ****************
type ILogWriter struct{}
// ILogWriter implements Writer
func (ILogWriter) Write(bs []byte) (int, error) {
// Print the byte-slice
fmt.Println(string(bs))
// A custom implementation
fmt.Print("Just wrote this many bytes: ", len(bs))
// Return
return len(bs), nil
}Then, we can make use of that in main()
func main() {
// Create an HTTP request
resp, err := http.Get("https://example.com")
// Error Handling
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
// Using a custom type that implements the Writer interface
lw := ILogWriter{}
io.Copy(lw, resp.Body)
}- REMEMBER
- Interfaces helps us go down the right path
- But they do not necssarily help us write correct code
- Implementing the logic correctly is still necessary