- Go Programming Blueprints(Second Edition)
- Mat Ryer
- 2437字
- 2021-07-08 10:40:04
Implementing Gravatar
Gravatar is a web service that allows users to upload a single profile picture and associate it with their e-mail address in order to make it available from any website. Developers, like us, can access these images for our application just by performing a GET
operation on a specific API endpoint. In this section, we will look at how to implement Gravatar rather than use the picture provided by the auth service.
Abstracting the avatar URL process
Since we have three different ways of obtaining the avatar URL in our application, we have reached the point where it would be sensible to learn how to abstract the functionality in order to cleanly implement the options. Abstraction refers to a process in which we separate the idea of something from its specific implementation. The http.Handler
method is a great example of how a handler will be used along with its ins and outs, without being specific about what action is taken by each handler.
In Go, we start to describe our idea of getting an avatar URL by defining an interface. Let's create a new file called avatar.go
and insert the following code:
package main import ( "errors" ) // ErrNoAvatar is the error that is returned when the // Avatar instance is unable to provide an avatar URL. var ErrNoAvatarURL = errors.New("chat: Unable to get an avatar URL.") // Avatar represents types capable of representing // user profile pictures. type Avatar interface { // GetAvatarURL gets the avatar URL for the specified client, // or returns an error if something goes wrong. // ErrNoAvatarURL is returned if the object is unable to get // a URL for the specified client. GetAvatarURL(c *client) (string, error) }
The Avatar
interface describes the GetAvatarURL
method that a type must satisfy in order to be able to get avatar URLs. We took the client as an argument so that we know the user for which the URL to be returned. The method returns two arguments: a string (which will be the URL if things go well) and an error in case something goes wrong.
One of the things that could go wrong is simply that one of the specific implementations of Avatar
is unable to get the URL. In that case, GetAvatarURL
will return the ErrNoAvatarURL
error as the second argument. The ErrNoAvatarURL
error therefore becomes a part of the interface; it's one of the possible returns from the method and something that users of our code should probably explicitly handle. We mention this in the comments part of the code for the method, which is the only way to communicate such design decisions in Go.
Tip
Because the error is initialized immediately using errors.New
and stored in the ErrNoAvatarURL
variable, only one of these objects will ever be created; passing the pointer of the error as a return is inexpensive. This is unlike Java's checked exceptions which serve a similar purpose where expensive exception objects are created and used as part of the control flow.
The auth service and the avatar's implementation
The first implementation of Avatar
we write will replace the existing functionality where we had hardcoded the avatar URL obtained from the auth service. Let's use a Test-driven Development (TDD) approach so that we can be sure our code works without having to manually test it. Let's create a new file called avatar_test.go
in the chat
folder:
package main import "testing" func TestAuthAvatar(t *testing.T) { var authAvatar AuthAvatar client := new(client) url, err := authAvatar.GetAvatarURL(client) if err != ErrNoAvatarURL { t.Error("AuthAvatar.GetAvatarURL should return ErrNoAvatarURL when no value present") } // set a value testUrl := "http://url-to-gravatar/" client.userData = map[string]interface{}{"avatar_url": testUrl} url, err = authAvatar.GetAvatarURL(client) if err != nil { t.Error("AuthAvatar.GetAvatarURL should return no error when value present") } if url != testUrl { t.Error("AuthAvatar.GetAvatarURL should return correct URL") } }
This file contains a test for our as-of-yet, nonexistent AuthAvatar
type's GetAvatarURL
method. First, it uses a client with no user data and ensures that the ErrNoAvatarURL
error is returned. After setting a suitable URL, our test calls the method again this time to assert that it returns the correct value. However, building this code fails because the AuthAvatar
type doesn't exist, so we'll declare authAvatar
next.
Before we write our implementation, it's worth noticing that we only declare the authAvatar
variable as the AuthAvatar
type but never actually assign anything to it so its value remains nil
. This is not a mistake; we are actually making use of Go's zero-initialization (or default initialization) capabilities. Since there is no state needed for our object (we will pass client
in as an argument), there is no need to waste time and memory on initializing an instance of it. In Go, it is acceptable to call a method on a nil
object, provided that the method doesn't try to access a field. When we actually come to writing our implementation, we will look at a way in which we can ensure this is the case.
Let's head back over to avatar.go
and make our test pass. Add the following code at the bottom of the file:
type AuthAvatar struct{} var UseAuthAvatar AuthAvatar func (AuthAvatar) GetAvatarURL(c *client) (string, error) { if url, ok := c.userData["avatar_url"]; ok { if urlStr, ok := url.(string); ok { return urlStr, nil } } return "", ErrNoAvatarURL }
Here, we define our AuthAvatar
type as an empty struct and define the implementation of the GetAvatarURL
method. We also create a handy variable called UseAuthAvatar
that has the AuthAvatar
type but which remains of nil
value. We can later assign the UseAuthAvatar
variable to any field looking for an Avatar
interface type.
Note
The GetAvatarURL
method we wrote earlier doesn't have a very nice line of sight; the happy return is buried within two if
blocks. See if you can refactor it so that the last line is return urlStr, nil
and the method exits early if the avatar_url
field is missing. You can refactor with confidence, since this code is covered by a unit test.
For a little more on the rationale behind this kind of refactor, refer to the article at http://bit.ly/lineofsightgolang.
Normally, the receiver of a method (the type defined in parentheses before the name) will be assigned to a variable so that it can be accessed in the body of the method. Since, in our case, we assume the object can have nil
value, we can omit a variable name to tell Go to throw away the reference. This serves as an added reminder to ourselves that we should avoid using it.
The body of our implementation is relatively simple otherwise: we are safely looking for the value of avatar_url
and ensuring that it is a string before returning it. If anything fails, we return the ErrNoAvatarURL
error, as defined in the interface.
Let's run the tests by opening a terminal and then navigating to the chat
folder and typing the following:
go test
If all is well, our tests will pass and we will have successfully created our first Avatar
implementation.
Using an implementation
When we use an implementation, we could refer to either the helper variables directly or create our own instance of the interface whenever we need the functionality. However, this would defeat the object of the abstraction. Instead, we use the Avatar
interface type to indicate where we need the capability.
For our chat application, we will have a single way to obtain an avatar URL per chat room. So, let's update the room
type so it can hold an Avatar
object. In room.go
, add the following field definition to the room struct
type:
// avatar is how avatar information will be obtained. avatar Avatar
Update the newRoom
function so that we can pass in an Avatar
implementation for use; we will just assign this implementation to the new field when we create our room
instance:
// newRoom makes a new room that is ready to go. func newRoom(avatar Avatar) *room { return &room{ forward: make(chan *message), join: make(chan *client), leave: make(chan *client), clients: make(map[*client]bool), tracer: trace.Off(), avatar: avatar, } }
Building the project now will highlight the fact that the call to newRoom
in main.go
is broken because we have not provided an Avatar
argument; let's update it by passing in our handy UseAuthAvatar
variable, as follows:
r := newRoom(UseAuthAvatar)
We didn't have to create an instance of AuthAvatar
, so no memory was allocated. In our case, this doesn't result in great saving (since we only have one room for our entire application), but imagine the size of the potential savings if our application has thousands of rooms. The way we named the UseAuthAvatar
variable means that the preceding code is very easy to read and it also makes our intention obvious.
Tip
Thinking about code readability is important when designing interfaces. Consider a method that takes a Boolean input just passing in true or false hides the real meaning if you don't know the argument names. Consider defining a couple of helper constants, as shown in the following short example:
func move(animated bool) { /* ... */ }
const Animate = true const
DontAnimate = false
Think about which of the following calls to move
are easier to understand:
move(true)
move(false)
move(Animate)
move(DontAnimate)
All that is left now is to change client
to use our new Avatar
interface. In client.go
, update the read
method, as follows:
func (c *client) read() { defer c.socket.Close() for { var msg *message if err := c.socket.ReadJSON(&msg); err != nil { return } msg.When = time.Now() msg.Name = c.userData["name"].(string) msg.AvatarURL, _ = c.room.avatar.GetAvatarURL(c) c.room.forward <- msg } }
Here, we are asking the avatar
instance in room
to get the avatar URL for us instead of extracting it from userData
ourselves.
When you build and run the application, you will notice that (although we have refactored things a little) the behavior and user experience hasn't changed at all. This is because we told our room to use the AuthAvatar
implementation.
Now let's add another implementation to the room.
The Gravatar implementation
The Gravatar implementation in Avatar
will do the same job as the AuthAvatar
implementation, except that it will generate a URL for a profile picture hosted on https://en.gravatar.com/. Let's start by adding a test to our avatar_test.go
file:
func TestGravatarAvatar(t *testing.T) { var gravatarAvatar GravatarAvatar client := new(client) client.userData = map[string]interface{}{"email": "MyEmailAddress@example.com"} url, err := gravatarAvatar.GetAvatarURL(client) if err != nil { t.Error("GravatarAvatar.GetAvatarURL should not return an error") } if url != "http://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346" { t.Errorf("GravatarAvatar.GetAvatarURL wrongly returned %s", url) } }
Gravatar uses a hash of the e-mail address to generate a unique ID for each profile picture, so we set up a client and ensure userData
contains an e-mail address. Next, we call the same GetAvatarURL
method, but this time on an object that has the GravatarAvatar
type. We then assert that a correct URL was returned. We already know this is the appropriate URL for the specified e-mail address because it is listed as an example in the Gravatar documentation a great strategy to ensure our code is doing what it should be doing.
Tip
Remember that all the source code for this book is available for download from the publishers and has also been published on GitHub. You can save time on building the preceding core by copying and pasting bits and pieces from https://github.com/matryer/goblueprints. Hardcoding things such as the base URL is not usually a good idea; we have hardcoded throughout the book to make the code snippets easier to read and more obvious, but you are welcome to extract them as you go along if you like.
Running these tests (with go test
) obviously causes errors because we haven't defined our types yet. Let's head back to avatar.go
and add the following code while being sure to import the io
package:
type GravatarAvatar struct{} var UseGravatar GravatarAvatar func(GravatarAvatar) GetAvatarURL(c *client) (string, error) { if email, ok := c.userData["email"]; ok { if emailStr, ok := email.(string); ok { m := md5.New() io.WriteString(m, strings.ToLower(emailStr)) return fmt.Sprintf("http://www.gravatar.com/avatar/%x", m.Sum(nil)), nil } } return "", ErrNoAvatarURL }
We used the same pattern as we did for AuthAvatar
: we have an empty struct, a helpful UseGravatar
variable, and the GetAvatarURL
method implementation itself. In this method, we follow Gravatar's guidelines to generate an MD5 hash from the e-mail address (after we ensured it was lowercase) and append it to the hardcoded base URL using fmt.Sprintf
.
Note
The preceding method also suffers from a bad line of sight in code. Can you live with it, or would you want to improve the readability somehow?
It is very easy to achieve hashing in Go thanks to the hard work put in by the writers of the Go standard library. The crypto
package has an impressive array of cryptography and hashing capabilities all very easy to use. In our case, we create a new md5
hasher and because the hasher implements the io.Writer
interface, we can use io.WriteString
to write a string of bytes to it. Calling Sum
returns the current hash for the bytes written.
Tip
You might have noticed that we end up hashing the e-mail address every time we need the avatar URL. This is pretty inefficient, especially at scale, but we should prioritize getting stuff done over optimization. If we need to, we can always come back later and change the way this works.
Running the tests now shows us that our code is working, but we haven't yet included an e-mail address in the auth
cookie. We do this by locating the code where we assign to the authCookieValue
object in auth.go
and updating it to grab the Email
value from Gomniauth:
authCookieValue := objx.New(map[string]interface{}{ "name": user.Name(), "avatar_url": user.AvatarURL(), "email": user.Email(), }).MustBase64()
The final thing we must do is tell our room to use the Gravatar implementation instead of the AuthAvatar
implementation. We do this by calling newRoom
in main.go
and making the following change:
r := newRoom(UseGravatar)
Build and run the chat program once again and head to the browser. Remember, since we have changed the information stored in the cookie, we must sign out and sign back in again in order to see our changes take effect.
Assuming you have a different image for your Gravatar account, you will notice that the system is now pulling the image from Gravatar instead of the auth provider. Using your browser's inspector or debug tool will show you that the src
attribute of the img
tag has indeed changed:

If you don't have a Gravatar account, you'll most likely see a default placeholder image in place of your profile picture.
- Designing Machine Learning Systems with Python
- Git Version Control Cookbook
- JavaScript:Functional Programming for JavaScript Developers
- PyTorch自動駕駛視覺感知算法實戰
- SAS數據統計分析與編程實踐
- 琢石成器:Windows環境下32位匯編語言程序設計
- Elasticsearch Server(Third Edition)
- Extending Puppet(Second Edition)
- 領域驅動設計:軟件核心復雜性應對之道(修訂版)
- C#應用程序設計教程
- 好好學Java:從零基礎到項目實戰
- Mastering Apache Storm
- Python Web自動化測試設計與實現
- JavaScript悟道
- Oracle Database XE 11gR2 Jump Start Guide