You are on page 1of 385

Go Programming Blueprints

Build real-world, production-ready solutions in Go using


cutting-edge technology and techniques
Published by Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham
B3 2PB, UK.

www.packtpub.com
matryer.com
@dahernan

@tylerb
@mazondo

@golangbridge
https://www.linkedin.com/in/ham
rah
www.PacktPub.com

www.PacktPub.com

service@packtpub.com

www.PacktPub.com

https://www.packtpub.com/mapt
Appendix
https://github.com/matryer/gobluep
rints
README
htt
ps://github.com/matryer/goblueprints
Chapter 1

Chapter 2

Chapter 3

Chapter 4

Chapter 5

Chapter 6
Chapter 5

http.HandlerFunc

Chapter 7
Chapter 8

Chapter 9

Chapter 10

Chapter 11
Chapter 9

Appendix

https://g
olang.org/doc/install#requirements

Appendix
webApp.war

package meander
type Cost int8
const (
_ Cost = iota
Cost1
Cost2
Cost3
Cost4
Cost5
)
feedback@packtpub.com

www.packtpub.com/authors

http://www.p
acktpub.com http://www.packtpub.c
om/support
https://github.com/PacktPubl
ishing/Go-Programming-Blueprints
https://github.com/PacktPublishing/

http://www.packtpub.com/submit-errata

https://www.packtpub.com/books/conten
t/support
copyright@packtpub.com

questions@packtpub.com


net/http

net/http

http.Handler
https://github.co
m/matryer/goblueprints/tree/master/chapter1/chat

GOPATH Appendix

main.go chat GOPATH

package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
<html>
<head>
<title>Chat</title>
</head>
<body>
Let's chat!
</body>
</html>
))
})
// start the web server
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

net/http

:8080 ListenAndServe

http.HandleFunc /
http://localhost:8080/
func(w http.ResponseWriter, r
*http.Request)

package main

package chat

main.go

go run

go build

http://localhost:8080
Hello {{name}}, how are you

{{name}}

Hello Bruce, how are you

text/template
html/template html/template

chat templates chat.html


main.go

<html>
<head>
<title>Chat</title>
</head>
<body>
Let's chat (from template)
</body>
</html>
struct
filename
sync.Once

text/template path/filepath sync

main.go func main()

// templ represents a single template


type templateHandler struct {
once sync.Once
filename string
templ *template.Template
}
// ServeHTTP handles the HTTP request.
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r
*http.Request) {
t.once.Do(func() {
t.templ = template.Must(template.ParseFiles(filepath.Join("templates",
t.filename)))
})
t.templ.Execute(w, nil)
}

Appendix

templateHandler ServeHTTP
http.HandleFunc

http.ResponseWriter ServeHTTP
http.Handler http.Handle

http://golang.org/pkg/net/http/#Handler
http.Handler ServeHTTP

net/http
NewTemplateHandler

main

ServeHTTP
sync.Once
ServeHTTP

ServeHTTP

ServeHTTP

templateHandler main

func main() {
// root
http.Handle("/", &templateHandler{filename: "chat.html"})
// start the web server
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

templateHandler http.Handler
http.Handle
templateHandler
chat.html &
http.Handle
templateHandler
go run
main.go

go build .go
-o

room
client
websocket

https://github.com/gorilla/websocket

client.go main.go chat

package main
import (
"github.com/gorilla/websocket"
)
// client represents a single chatting user.
type client struct {
// socket is the web socket for this client.
socket *websocket.Conn
// send is a channel on which messages are sent.
send chan []byte
// room is the room this client is chatting in.
room *room
}
socket
send

room

go get websocket

room
room.go

package main
type room struct {
// forward is a channel that holds incoming messages
// that should be forwarded to the other clients.
forward chan []byte
}

forward

client.go
client read write
client

func (c *client) read() {


defer c.socket.Close()
for {
_, msg, err := c.socket.ReadMessage()
if err != nil {
return
}
c.room.forward <- msg
}
}
func (c *client) write() {
defer c.socket.Close()
for msg := range c.send {
err := c.socket.WriteMessage(websocket.TextMessage, msg)
if err != nil {
return
}
}
}

read ReadMessage
forward room
'the socket has died'
write send
WriteMessage
for

defer
c.socket.Close()

return
defer
defer
close
c.room.forward <- msg

room.go

package main
type room struct {
// forward is a channel that holds incoming messages
// that should be forwarded to the other clients.
forward chan []byte
// join is a channel for clients wishing to join the room.
join chan *client
// leave is a channel for clients wishing to leave the room.
leave chan *client
// clients holds all current clients in this room.
clients map[*client]bool
}

join leave
clients

select
select

room run select

func (r *room) run() {


for {
select {
case client := <-r.join:
// joining
r.clients[client] = true
case client := <-r.leave:
// leaving
delete(r.clients, client)
close(client.send)
case msg := <-r.forward:
// forward message to all clients
for client := range r.clients {
client.send <- msg
}
}
}
}

for

join leave forward


select

r.clients

join r.clients

true
true

leave client
send forward
send
write

room http.Handler

ServeHTTP
room.go

const (
socketBufferSize = 1024
messageBufferSize = 256
)
var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize,
WriteBufferSize: socketBufferSize}
func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
socket, err := upgrader.Upgrade(w, req, nil)
if err != nil {
log.Fatal("ServeHTTP:", err)
return
}
client := &client{
socket: socket,
send: make(chan []byte, messageBufferSize),
room: r,
}
r.join <- client
defer func() { r.leave <- client }()
go client.write()
client.read()
}

ServeHTTP

websocket.Upgrader
ServeHTTP
upgrader.Upgrade
join

write
go go
read

r := &room{
forward: make(chan []byte),
join: make(chan *client),
leave: make(chan *client),
clients: make(map[*client]bool),
}

newRoom

type room struct

// newRoom makes a new room.


func newRoom() *room {
return &room{
forward: make(chan []byte),
join: make(chan *client),
leave: make(chan *client),
clients: make(map[*client]bool),
}
}

newRoom
main main.go

func main() {
r := newRoom()
http.Handle("/", &templateHandler{filename: "chat.html"})
http.Handle("/room", r)
// get the room going
go r.run()
// start the web server
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

go

chat.html templates

<html>
<head>
<title>Chat</title>
<style>
input { display: block; }
ul { list-style: none; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="chatbox">
<textarea></textarea>
<input type="submit" value="Send" />
</form> </body>
</html>

messages

form </body>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<script>
$(function(){
var socket = null;
var msgBox = $("#chatbox textarea");
var messages = $("#messages");
$("#chatbox").submit(function(){
if (!msgBox.val()) return false;
if (!socket) {
alert("Error: There is no socket connection.");
return false;
}
socket.send(msgBox.val());
msgBox.val("");
return false;
});
if (!window["WebSocket"]) {
alert("Error: Your browser does not support web sockets.")
} else {
socket = new WebSocket("ws://localhost:8080/room");
socket.onclose = function() {
alert("Connection has been closed.");
}
socket.onmessage = function(e) {
messages.append($("<li>").text(e.data));
}
}
});
</script>

socket = new WebSocket("ws://localhost:8080/room")


onclose onmessage
socket.send

http://localhost:8080/
:8080
main.go

if err := http.ListenAndServe(":8080", nil); err != nil {


log.Fatal("ListenAndServe:", err)
}

socket = new WebSocket("ws://localhost:8080/room");

8080

main main.go

func main() {
var addr = flag.String("addr", ":8080", "The addr of the application.")
flag.Parse() // parse the flags
r := newRoom()
http.Handle("/", &templateHandler{filename: "chat.html"})
http.Handle("/room", r)
// get the room going
go r.run()
// start the web server
log.Println("Starting web server on", *addr)
if err := http.ListenAndServe(*addr, nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}

flag
addr :8080
flag.Parse()

*addr

flag.String *string

*
log.Println

templateHandler
Execute main.go
ServeHTTP r data Execute

func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r


*http.Request) {
t.once.Do(func() {
t.templ = template.Must(template.ParseFiles(filepath.Join("templates",
t.filename)))
})
t.templ.Execute(w, r)
}

http.Request

Host http.Request

chat.html

socket = new WebSocket("ws://{{.Host}}/room");

{{.Host}}
request.Host r

text/template

http://golang.org/pkg/text/template

go build -o chat
./chat -addr=":3000"

{{.Host}}

-addr="192.168.0.1:3000"
log.Println

main

Tracer

templateHandler
trace
chat

/chat
client.go
main.go
room.go
/trace

tracer.go trace

package trace
// Tracer is the interface that describes an object capable of
// tracing events throughout code.
type Tracer interface {
Trace(...interface{})
}

trace
Tracer T
Trace ...interface{}
Trace

fmt.Sprint log.Fatal

Tracer

tracer_test.go trace

package trace
import (
"testing"
)
func TestNew(t *testing.T) {
t.Error("We haven't written our test yet")
}

_test.go Test
*testing.T
trace
t.Error TestNew

cls clear

TestNew

func TestNew(t *testing.T) {


var buf bytes.Buffer
tracer := New(&buf)
if tracer == nil {
t.Error("Return from New should not be nil")
} else {
tracer.Trace("Hello trace package.")
if buf.String() != "Hello trace package.\n" {
t.Errorf("Trace should not write '%s'.", buf.String())
}
}

import
go get
import "bytes"

bytes.Buffer
t.Errorf
New
nil t.Error
go test New

Consider a meaningless test for a minute:


if true == true {
t.Error("True should be true")
}

true true false

true true

go test
New
trace.go

func New() {}

go test
New New
New
New

New

func New(w io.Writer) {}

io.Writer
Write

io.Writer
bytes.Buffer

io.Writer

go test

func New(w io.Writer) Tracer {}

New Tracer
go test

nil New

func New(w io.Writer) Tracer {


return nil
}
nil go test

-cover

New
Tracer
tracer.go

type tracer struct {


out io.Writer
}
func (t *tracer) Trace(a ...interface{}) {}

tracer io.Writer
out Trace
Tracer

New

func New(w io.Writer) Tracer {


return &tracer{out: w}
}

go test
Trace
Trace io.Writer

func (t *tracer) Trace(a ...interface{}) {


fmt.Fprint(t.out, a...)
fmt.Fprintln(t.out)
}

Trace fmt.Fprint fmt.Fprintln


out

tracer t
New

Tracer
tracer
tracer

ioutil.NopCloser
io.Reader io.ReadCloser Close
io.Reader
io.ReadCloser io.ReadCloser
nopCloser
http://golang.org/src/pkg/io/ioutil/ioutil.go
nopCloser

trace

room.go Trace
trace GOPATH
$GOPATH/src trace
$GOPATH/src/mycode/trace mycode/trace

room run()

type room struct {


// forward is a channel that holds incoming messages
// that should be forwarded to the other clients.
forward chan []byte
// join is a channel for clients wishing to join the room.
join chan *client
// leave is a channel for clients wishing to leave the room.
leave chan *client
// clients holds all current clients in this room.
clients map[*client]bool
// tracer will receive trace information of activity
// in the room.
tracer trace.Tracer
}
func (r *room) run() {
for {
select {
case client := <-r.join:
// joining
r.clients[client] = true
r.tracer.Trace("New client joined")
case client := <-r.leave:
// leaving
delete(r.clients, client)
close(client.send)
r.tracer.Trace("Client left")
case msg := <-r.forward:
r.tracer.Trace("Message received: ", string(msg))
// forward message to all clients
for client := range r.clients {
client.send <- msg
r.tracer.Trace(" -- sent to client")
}
}
}
}

trace.Tracer room
Trace
tracer nil

room main.go

r := newRoom()
r.tracer = trace.New(os.Stdout)

New os.Stdout
room
trace trace.Off()
Tracer
Trace

Off Trace

tracer_test.go

func TestOff(t *testing.T) {


var silentTracer Tracer = Off()
silentTracer.Trace("something")
}

tracer.go

type nilTracer struct{}

func (t *nilTracer) Trace(a ...interface{}) {}

// Off creates a Tracer that will ignore calls to Trace.


func Off() Tracer {
return &nilTracer{}
}

nilTracer Trace
Off() nilTracer
nilTracer tracer
io.Writer
newRoom room.go

func newRoom() *room {


return &room{
forward: make(chan []byte),
join: make(chan *client),
leave: make(chan *client),
clients: make(map[*client]bool),
tracer: trace.Off(),
}
}

room nilTracer Trace


r.tracer =
trace.New(os.Stdout) main.go

trace

New() –
Off() –
Tracer –

http:
//blog.golang.org/godoc-documenting-go-code
tracer.go
trace htt
ps://github.com/matryer/goblueprints/blob/master/chapter1/trac
e/tracer.go
net/http

http.Handler trace.Tracer

ServeHTTP room

http.Handler

gomniauth
http

http.Handler

http.Handler
Chaining pattern when applied to HTTP handlers

http.Handler
http.Handle

Logging ServeHTTP
http.Handler
Logging

auth.go
chat

package main
import ("net/http")
type authHandler struct {
next http.Handler
}
func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_, err := r.Cookie("auth")
if err == http.ErrNoCookie {
// not authenticated
w.Header().Set("Location", "/login")
w.WriteHeader(http.StatusTemporaryRedirect)
return
}
if err != nil {
// some other error
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// success - call the next handler
h.next.ServeHTTP(w, r)
}
func MustAuth(handler http.Handler) http.Handler {
return &authHandler{next: handler}
}

authHandler ServeHTTP
http.Handler http.Handler next
MustAuth authHandler
http.Handler
main.go

http.Handle("/", &templateHandler{filename: "chat.html"})

MustAuth templateHandler

http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"}))

templateHandler MustAuth
authHandler templateHandler

ServeHTTP authHandler auth


Header WriteHeader http.ResponseWriter

http://localhost:8080/chat
/login

assets main
http.Handle
http.StripPrefix http.FileServer
http.Handler
MustAuth

main.go

http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"}))


http.Handle("/login", &templateHandler{filename: "login.html"})
http.Handle("/room", r)

MustAuth

login.html templates

<html>
<head>
<title>Login</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com
/bootstrap/3.3.6/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="page-header">
<h1>Sign in</h1>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">In order to chat, you must be signed
in</h3>
</div>
<div class="panel-body">
<p>Select the service you would like to sign in with:</p>
<ul>
<li>
<a href="/auth/login/facebook">Facebook</a>
</li>
<li>
<a href="/auth/login/github">GitHub</a>
</li>
<li>
<a href="/auth/login/google">Google</a>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>

http://localhost:8080/login

http

"auth/:action/:provider_name"

auth/login/google
params[:provider_name] google params[:action]
login
http

"auth/"

/auth/login/google
/auth/login/facebook
/auth/callback/google
/auth/callback/facebook

goweb pat routes mux

auth.go
loginHandler

// loginHandler handles the third-party login process.


// format: /auth/{action}/{provider}
func loginHandler(w http.ResponseWriter, r *http.Request) {
segs := strings.Split(r.URL.Path, "/")
action := segs[2]
provider := segs[3]
switch action {
case "login":
log.Println("TODO handle login for", provider)
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Auth action %s not supported", action)
}
}

strings.Split
action provider

http.StatusNotFound 404
loginHandler
segs[2] segs[3]

/auth/nonsense

loginHandler
http.Handler
http.HandleFunc
http.Handle main.go

http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"}))


http.Handle("/login", &templateHandler{filename: "login.html"})
http.HandleFunc("/auth/", loginHandler)
http.Handle("/room", r)

http://localhost:8080/auth/login/google TODO handle login


for google
http://localhost:8080/auth/login/facebook TODO handle
login for facebook

TODO
goauth2 https://github.com
/golang/oauth2

gomniauth https://github.com/stretchr/gomniauth
omniauth gomniauth

gomniauth
gomniauth

gomniauth
http://wiki.bazaar.canonic
al.com
localhost:8080

loginHandler
http://localhost:8080/auth/callback/google

gomniauth
WithProviders
gomniauth main.go
flag.Parse() main

// setup gomniauth
gomniauth.SetSecurityKey("PUT YOUR AUTH KEY HERE")
gomniauth.WithProviders(
facebook.New("key", "secret",
"http://localhost:8080/auth/callback/facebook"),
github.New("key", "secret",
"http://localhost:8080/auth/callback/github"),
google.New("key", "secret",
"http://localhost:8080/auth/callback/google"),
)
key secret

callback

import (
"github.com/stretchr/gomniauth/providers/facebook"
"github.com/stretchr/gomniauth/providers/github"
"github.com/stretchr/gomniauth/providers/google"
)

SetSecurityKey

some long key

/auth/login/{provider}
loginHandler auth.go

func loginHandler(w http.ResponseWriter, r *http.Request) {


segs := strings.Split(r.URL.Path, "/")
action := segs[2]
provider := segs[3]
switch action {
case "login":
provider, err := gomniauth.Provider(provider)
if err != nil {
http.Error(w, fmt.Sprintf("Error when trying to get provider
%s: %s",provider, err), http.StatusBadRequest)
return
}
loginUrl, err := provider.GetBeginAuthURL(nil, nil)
if err != nil {
http.Error(w, fmt.Sprintf("Error when trying to GetBeginAuthURL
for %s:%s", provider, err), http. StatusInternalServerError)
return
}
w.Header.Set("Location", loginUrl)
w.WriteHeader(http.StatusTemporaryRedirect)
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Auth action %s not supported", action)
}
}

gomniauth.Provider
google github
GetBeginAuthURL

GetBeginAuthURL(nil, nil)

/chat

scope

GetBeginAuthURL

http.Error
non-200
https://githu
b.com/pilu/fresh https://github.com/codegangsta/gin

http://localhost:8080/chat
Auth action callback not supported
loginHandler

http://localhost:8080/auth/callback/googlecode=4/Q92xJ-
BQfoX6PHhzkjhgtyfLc0Ylm.QqV4u9AbA9sYguyfbjFEsNoJKMOjQI

auth.go

case "callback":
provider, err := gomniauth.Provider(provider)
if err != nil {
http.Error(w, fmt.Sprintf("Error when trying to get provider %s: %s",
provider, err), http.StatusBadRequest)
return
}
creds, err :=
provider.CompleteAuth(objx.MustFromURLQuery(r.URL.RawQuery))
if err != nil {
http.Error(w, fmt.Sprintf("Error when trying to complete auth for
%s: %s", provider, err), http.StatusInternalServerError)
return
}
user, err := provider.GetUser(creds)
if err != nil {
http.Error(w, fmt.Sprintf("Error when trying to get user from %s: %s",
provider, err), http.StatusInternalServerError)
return
}
authCookieValue := objx.New(map[string]interface{}{
"name": user.Name(),
}).MustBase64()
http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: authCookieValue,
Path: "/"})
w.Header().Set("Location", "/chat")
w.WriteHeader(http.StatusTemporaryRedirect)

CompleteAuth RawQuery
objx.Map CompleteAuth

GetUser

Name
auth

/chat


auth
eyJuYW1lIjoiTWF0IFJ5ZXIifQ==
{"name":"Mat Ryer"}

templateHandler Execute

ServeHTTP templateHandler main.go

func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r


*http.Request) {
t.once.Do(func() {
t.templ = template.Must(template.ParseFiles(filepath.Join("templates",
t.filename)))
})
data := map[string]interface{}{
"Host": r.Host,
}
if authCookie, err := r.Cookie("auth"); err == nil {
data["UserData"] = objx.MustFromBase64(authCookie.Value)
}
t.templ.Execute(w, data)
}

http.Request
map[string]interface{}
Host UserData auth
Host
make
data Execute
chatbox chat.html

<form id="chatbox">
{{.UserData.name}}:<br/>
<textarea></textarea>
<input type="submit" value="Send" />
</form>

{{.UserData.name}}
textarea

objx go get
http://github.com/stretchr/objx

vendor

$GOPATH go get

https://blog.gopheracademy.com/advent-2015/vendor-folder/
vendoring in Go

[]byte
chan
[]byte
[]byte
message.go chat

package main
import (
"time"
)
// message represents a single message
type message struct {
Name string
Message string
When time.Time
}

message
Name When

client

read write
client.go ReadJSON WriteJSON
message

func (c *client) read() {


defer c.socket.Close()
for {
var msg *message
err := c.socket.ReadJSON(&msg)
if err != nil {
return
}
msg.When = time.Now()
msg.Name = c.userData["name"].(string)
c.room.forward <- msg
}
}
func (c *client) write() {
defer c.socket.Close()
for msg := range c.send {
err := c.socket.WriteJSON(msg)
if err != nil {
break
}
}
}

Message
When Name

*message forward
send chan []byte
room.go forward chan *message
send chan client.go

room.go

forward: make(chan []byte) forward: make(chan *message)


r.tracer.Trace("Message received: ", string(msg))
r.tracer.Trace("Message received: ", msg.Message)
send: make(chan []byte, messageBufferSize) send:
make(chan *message, messageBufferSize)

client
client
map[string]interface{} userData

// client represents a single chatting user.


type client struct {
// socket is the web socket for this client.
socket *websocket.Conn
// send is a channel on which messages are sent.
send chan *message
// room is the room this client is chatting in.
room *room
// userData holds information about the user
userData map[string]interface{}
}
http.Request
Cookie room.go ServeHTTP

func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) {


socket, err := upgrader.Upgrade(w, req, nil)
if err != nil {
log.Fatal("ServeHTTP:", err)
return
}
authCookie, err := req.Cookie("auth")
if err != nil {
log.Fatal("Failed to get auth cookie:", err)
return
}
client := &client{
socket: socket,
send: make(chan *message, messageBufferSize),
room: r,
userData: objx.MustFromBase64(authCookie.Value),
}
r.join <- client
defer func() { r.leave <- client }()
go client.write()
client.read()
}

Cookie http.Request
objx.MustFromBase64

[]byte
*message

chat.html socket.send

socket.send(JSON.stringify({"Message": msgBox.val()}));

JSON.stringify
Message
message
message
socket.onmessage

socket.onmessage = function(e) {
var msg = JSON.parse(e.data);
messages.append(
$("<li>").append(
$("<strong>").text(msg.Name + ": "),
$("<span>").text(msg.Message)
)
);
}

JSON.parse
Gomniauth

http.Handler

auth MustAuth

message

message chan *message

time.Time
message


https://en.gravatar.com/

https://en.gravatar.com/
https://en.gravatar.com/

struct

avatar_url picture
url picture
GetUser
auth.go callback
authCookieValue

authCookieValue := objx.New(map[string]interface{}{
"name": user.Name(),
"avatar_url": user.AvatarURL(),
}).MustBase64()

AvatarURL
avatar_url

User
map[string]interface{}

message
message.go AvatarURL

type message struct {


Name string
Message string
When time.Time
AvatarURL string
}

AvatarURL Name
read client.go

func (c *client) read() {


defer c.socket.Close()
for {
var msg *message
err := c.socket.ReadJSON(&msg)
if err != nil {
return
}
msg.When = time.Now()
msg.Name = c.userData["name"].(string)
if avatarURL, ok := c.userData["avatar_url"]; ok {
msg.AvatarURL = avatarURL.(string)
}
c.room.forward <- msg
}
}

userData
message

nil string

socket.onmessage
chat.html

socket.onmessage = function(e) {
var msg = JSON.parse(e.data);
messages.append(
$("<li>").append(
$("<img>").css({
width:50,
verticalAlign:"middle"
}).attr("src", msg.AvatarURL),
$("<strong>").text(msg.Name + ": "),
$("<span>").text(msg.Message)
)
);
}

img AvatarURL
css 50
auth
avatar_url

auth

HandleFunc main.go

http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {


http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: "",
Path: "/",
MaxAge: -1,
})
w.Header().Set("Location", "/chat")
w.WriteHeader(http.StatusTemporaryRedirect)
})

http.SetCookie MaxAge
-1
Value

ServeHTTP authHandler
auth.go

if cookie, err := r.Cookie("auth"); err ==


http.ErrNoCookie || cookie.Value == ""
r.Cookie

Value
Sign Out
chat.html chatbox
/logout

<form id="chatbox">
{{.UserData.name}}:<br/>
<textarea></textarea>
<input type="submit" value="Send" />
or <a href="/logout">sign out</a>
</form>

localhost:8080/chat
chat.html

style link

<link rel="stylesheet"href="//netdna.bootstrapcdn.com/bootstrap
/3.3.6/css/bootstrap.min.css">
<style>
ul#messages { list-style: none; }
ul#messages li { margin-bottom: 2px; }
ul#messages li img { margin-right: 10px; }
</style>

body script

<div class="container">
<div class="panel panel-default">
<div class="panel-body">
<ul id="messages"></ul>
</div>
</div>
<form id="chatbox" role="form">
<div class="form-group">
<label for="message">Send a message as {{.UserData.name}}
</label> or <a href="/logout">Sign out</a>
<textarea id="message" class="form-control"></textarea>
</div>
<input type="submit" value="Send" class="btn btn-default" />
</form>
</div>
socket.onmessage

socket.onmessage = function(e) {
var msg = JSON.parse(e.data);
messages.append(
$("<li>").append(
$("<img>").attr("title", msg.Name).css({
width:50,
verticalAlign:"middle"
}).attr("src", msg.AvatarURL),
$("<span>").text(msg.Message)
)
);
}
GET

http.Handler

avatar.go

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)
}

Avatar GetAvatarURL
Avatar GetAvatarURL
ErrNoAvatarURL ErrNoAvatarURL

errors.New
ErrNoAvatarURL

Avatar

avatar_test.go chat

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")
}
}
AuthAvatar GetAvatarURL
ErrNoAvatarURL

AuthAvatar
authAvatar

authAvatar AuthAvatar
nil

client
nil

avatar.go

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
}

AuthAvatar
GetAvatarURL UseAuthAvatar
AuthAvatar nil
UseAuthAvatar Avatar

GetAvatarURL
if
return urlStr, nil
avatar_url

http://bit.ly/lineofsightgolang
nil

avatar_url
ErrNoAvatarURL

chat

Avatar

Avatar

room Avatar room.go


room struct

// avatar is how avatar information will be obtained.


avatar Avatar

newRoom Avatar
room

// 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,
}
}

newRoom main.go
Avatar
UseAuthAvatar

r := newRoom(UseAuthAvatar)

AuthAvatar

UseAuthAvatar

func move(animated bool) { /* ... */ }


const Animate = true const
DontAnimate = false
move
move(true)
move(false)
move(Animate)
move(DontAnimate)

client Avatar client.go


read

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
}
}
avatar room
userData

AuthAvatar

Avatar AuthAvatar
https://
en.gravatar.com/ avatar_test.go

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 != "//www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346" {
t.Errorf("GravatarAvatar.GetAvatarURL wrongly returned %s", url)
}
}

userData
GetAvatarURL GravatarAvatar
https://github.com/matryer/goblueprints

go test
avatar.go
io

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("//www.gravatar.com/avatar/%x", m.Sum(nil)), nil
}
}
return "", ErrNoAvatarURL
}

AuthAvatar
UseGravatar GetAvatarURL

fmt.Sprintf
crypto
md5
io.Writer io.WriteString
Sum

auth
authCookieValue auth.go Email

authCookieValue := objx.New(map[string]interface{}{
"name": user.Name(),
"avatar_url": user.AvatarURL(),
"email": user.Email(),
}).MustBase64()

AuthAvatar newRoom main.go

r := newRoom(UseGravatar)

src img
GravatarAuth

auth.go authCookieValue

m := md5.New()
io.WriteString(m, strings.ToLower(user.Email()))
userId := fmt.Sprintf("%x", m.Sum(nil))
authCookieValue := objx.New(map[string]interface{}{
"userid": userId,
"name": user.Name(),
"avatar_url": user.AvatarURL(),
"email": user.Email(),
}).MustBase64()

userid

avatar_test.go

client.userData = map[string]interface{}{"email":
"MyEmailAddress@example.com"}

client.userData = map[string]interface{}{"userid":
"0bc83cb571cd1c50ba6f3e8a78ef1346"}

email
userid go test

avatar.go GetAvatarURL
GravatarAuth

func(GravatarAvatar) GetAvatarURL(c *client) (string, error) {


if userid, ok := c.userData["userid"]; ok {
if useridStr, ok := userid.(string); ok {
return "//www.gravatar.com/avatar/" + useridStr, nil
}
}
return "", ErrNoAvatarURL
}

chat/templates upload.html

<html>
<head>
<title>Upload</title>
<link rel="stylesheet"
href="//netdna.bootstrapcdn.com/bootstrap/3.6.6/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="page-header">
<h1>Upload picture</h1>
</div>
<form role="form" action="/uploader" enctype="multipart/form-data"
method="post">
<input type="hidden" name="userid" value="{{.UserData.userid}}" />
<div class="form-group">
<label for="avatarFile">Select file</label>
<input type="file" name="avatarFile" />
</div>
<input type="submit" value="Upload" class="btn" />
</form>
</div>
</body>
</html>
/uploader
enctype multipart/form-data
input
file
userid UserData
name

/upload main.go

http.Handle("/upload", &templateHandler{filename: "upload.html"})

/uploader
HandlerFunc

chat avatars

upload.go
ioutils net/http io
path

func uploaderHandler(w http.ResponseWriter, req *http.Request) {


userId := req.FormValue("userid")
file, header, err := req.FormFile("avatarFile")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data, err := ioutil.ReadAll(file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filename := path.Join("avatars", userId+path.Ext(header.Filename))
err = ioutil.WriteFile(filename, data, 0777)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
io.WriteString(w, "Successful")
}

uploaderHandler FormValue http.Request


io.Reader
req.FormFile
multipart.File
io.Reader multipart.FileHeader

nil

multipart.File
io.Reader http://golang.org/pkg/mime/m
ultipart/#File
multipart.File
io.Reader multipart.File
io.Reader

multipart.File io.Reader

Read

ioutil.ReadAll io.Reader

path.Join path.Ext
userid
multipart.FileHeader

ioutil.WriteFile avatars
userid
0777
http.StatusInternalServerError

/uploader main.go
func main

http.HandleFunc("/uploader", uploaderHandler)

auth

http://localhost:8080/upload
chat/avatars
userid

net/http
main.go

http.Handle("/avatars/",
http.StripPrefix("/avatars/",
http.FileServer(http.Dir("./avatars"))))

http.Handle
/avatars/
http.StripPrefix http.FileServer http.Handler
StripPrefix
http.Handler
http.FileServer
404 Not
Found http.Dir
/avatars/
http.StripPrefix
avatars avatars
/avatars/avatars/filename /avatars/filename

http://localhost:8080/avatars/

avatars

http://localhost:8080/upload

Avatar

avatar_test.go

func TestFileSystemAvatar(t *testing.T) {


filename := filepath.Join("avatars", "abc.jpg")
ioutil.WriteFile(filename, []byte{}, 0777)
defer os.Remove(filename)
var fileSystemAvatar FileSystemAvatar
client := new(client)
client.userData = map[string]interface{}{"userid": "abc"}
url, err := fileSystemAvatar.GetAvatarURL(client)
if err != nil {
t.Error("FileSystemAvatar.GetAvatarURL should not return an error")
}
if url != "/avatars/abc.jpg" {
t.Errorf("FileSystemAvatar.GetAvatarURL wrongly returned %s", url)
}
}

GravatarAvatar
avatars
userid client.userData
GetAvatarURL
avatar.go

type FileSystemAvatar struct{}


var UseFileSystemAvatar FileSystemAvatar
func (FileSystemAvatar) GetAvatarURL(c *client) (string, error) {
if userid, ok := c.userData["userid"]; ok {
if useridStr, ok := userid.(string); ok {
return "/avatars/" + useridStr + ".jpg", nil
}
}
return "", ErrNoAvatarURL
}

userid

.jpg

main.go Avatar

r := newRoom(UseFileSystemAvatar)

http://localhost:8080/upload

http://localhost:8080/chat

/upload
/chat
GetAvatarURL
FileSystemAvatar

ioutil.ReadDir

IsDir

userid
path.Match userid

ErrNoAvatarURL

avatar.go

func (FileSystemAvatar) GetAvatarURL(c *client) (string, error) {


if userid, ok := c.userData["userid"]; ok {
if useridStr, ok := userid.(string); ok {
files, err := ioutil.ReadDir("avatars")
if err != nil {
return "", ErrNoAvatarURL
}
for _, file := range files {
if file.IsDir() {
continue
}
if match, _ := path.Match(useridStr+"*", file.Name());
match {
return "/avatars/" + file.Name(), nil
}
}
}
}
return "", ErrNoAvatarURL
}

avatar

Avatar
GetAvatarURL
avatars

auth Avatar
client GetAvatarURL

Avatar
GetAvatarURL

Avatar

Avatar

auth.go package

import gomniauthcommon "github.com/stretchr/gomniauth/common"


type ChatUser interface {
UniqueID() string
AvatarURL() string
}
type chatUser struct {
gomniauthcommon.User
uniqueID string
}
func (u chatUser) UniqueID() string {
return u.uniqueID
}

import common
gomniauthcommon

ChatUser
Avatar
chatUser

gomniauth/common.User struct
ChatUser User
AvatarURL
chatUser User
User ChatUser

Avatar

avatar_test.go TestAuthAvatar

func TestAuthAvatar(t *testing.T) {


var authAvatar AuthAvatar
testUser := &gomniauthtest.TestUser{}
testUser.On("AvatarURL").Return("", ErrNoAvatarURL)
testChatUser := &chatUser{User: testUser}
url, err := authAvatar.GetAvatarURL(testChatUser)
if err != ErrNoAvatarURL {
t.Error("AuthAvatar.GetAvatarURL should return ErrNoAvatarURL
when no value present")
}
testUrl := "http://url-to-gravatar/"
testUser = &gomniauthtest.TestUser{}
testChatUser.User = testUser
testUser.On("AvatarURL").Return(testUrl, nil)
url, err = authAvatar.GetAvatarURL(testChatUser)
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")
}
}

gomniauth/test
gomniauthtest
TestUser chatUser
chatUser GetAvatarURL

TestUser Testify
https://github.com/stretchr/
testify
On Return TestUser
AvatarURL

testUrl

UniqueID

avatar_test.go

func TestGravatarAvatar(t *testing.T) {


var gravatarAvatar GravatarAvatar
user := &chatUser{uniqueID: "abc"}
url, err := gravatarAvatar.GetAvatarURL(user)
if err != nil {
t.Error("GravatarAvatar.GetAvatarURL should not return an error")
}
if url != "//www.gravatar.com/avatar/abc" {
t.Errorf("GravatarAvatar.GetAvatarURL wrongly returned %s", url)
}
}
func TestFileSystemAvatar(t *testing.T) {
// make a test avatar file
filename := path.Join("avatars", "abc.jpg")
ioutil.WriteFile(filename, []byte{}, 0777)
defer func() { os.Remove(filename) }()
var fileSystemAvatar FileSystemAvatar
user := &chatUser{uniqueID: "abc"}
url, err := fileSystemAvatar.GetAvatarURL(user)
if err != nil {
t.Error("FileSystemAvatar.GetAvatarURL should not return an error")
}
if url != "/avatars/abc.jpg" {
t.Errorf("FileSystemAvatar.GetAvatarURL wrongly returned %s", url)
}
}

Avatar
avatar.go GetAvatarURL Avatar
ChatUser client

GetAvatarURL(ChatUser) (string, error)

ChatUser
chatUser
GetAvatarURL

GetAvatarURL client

FileSystemAvatar

func (FileSystemAvatar) GetAvatarURL(u ChatUser) (string, error) {


if files, err := ioutil.ReadDir("avatars"); err == nil {
for _, file := range files {
if file.IsDir() {
continue
}
if match, _ := path.Match(u.UniqueID()+"*", file.Name());
match {
return "/avatars/" + file.Name(), nil
}
}
}
return "", ErrNoAvatarURL
}
userData
UniqueID ChatUser

AuthAvatar

func (AuthAvatar) GetAvatarURL(u ChatUser) (string, error) {


url := u.AvatarURL()
if len(url) == 0 {
return "", ErrNoAvatarURL
}
return url, nil
}

AvatarURL
ErrNoAvatarURL

if

if...else

GravatarAvatar

func (GravatarAvatar) GetAvatarURL(u ChatUser) (string, error) {


return "//www.gravatar.com/avatar/" + u.UniqueID(), nil
}

Avatar room

Avatar
Avatar
import main.go

// set the active Avatar implementation


var avatars Avatar = UseFileSystemAvatar

avatars

GetAvatarURL
userData auth
msg.AvatarURL

if avatarUrl, ok := c.userData["avatar_url"]; ok {
msg.AvatarURL = avatarUrl.(string)
}

loginHandler auth.go provider.GetUser


authCookieValue

user, err := provider.GetUser(creds)


if err != nil {
log.Fatalln("Error when trying to get user from", provider, "-", err)
}
chatUser := &chatUser{User: user}
m := md5.New()
io.WriteString(m, strings.ToLower(user.Email()))
chatUser.uniqueID = fmt.Sprintf("%x", m.Sum(nil))
avatarURL, err := avatars.GetAvatarURL(chatUser)
if err != nil {
log.Fatalln("Error when trying to GetAvatarURL", "-", err)
}

chatUser User
User
userid uniqueID
avatars.GetAvatarURL
authCookieValue
auth.go

authCookieValue := objx.New(map[string]interface{}{
"userid": chatUser.uniqueID,
"name": user.Name(),
"avatar_url": avatarURL,
}).MustBase64()

Avatar

Avatar room
room.go avatar Avatar
room newRoom

func newRoom() *room {


return &room{
forward: make(chan *message),
join: make(chan *client),
leave: make(chan *client),
clients: make(map[*client]bool),
tracer: trace.Off(),
}
}
main.go newRoom

golint go vet

Avatar

ErrNoAvatarURL

avatar.go Avatar

type TryAvatars []Avatar

TryAvatars Avatar
GetAvatarURL

func (a TryAvatars) GetAvatarURL(u ChatUser) (string, error) {


for _, avatar := range a {
if url, err := avatar.GetAvatarURL(u); err == nil {
return url, nil
}
}
return "", ErrNoAvatarURL
}
TryAvatars Avatar

Avatar GetAvatarURL

ErrNoAvatarURL

avatars main.go

var avatars Avatar = TryAvatars{


UseFileSystemAvatar,
UseAuthAvatar,
UseGravatar}

TryAvatars
Avatar

http://localhost:8080/logout
avatars
http://localhost:8080/chat

http://localhost:8080/upload
Avatar

https://en.gravatar.com/

avatars
http.FileServer

GetAvatarURL

ErrNoAvatarURL

Avatars
Avatar Avatar

ErrNoAvatarURL

stdin
stdout

NUL /dev/null
|

echo Hello
md5
Hello

.com .net
.com
chat chatapp
talk talk time

math/rand

bufio stdin fmt.Println


stdout
math/rand

$GOPATH/src
~/Work/projects/go
~/Work/projects/go/src

$GOPATH/src sprinkle main.go

package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strings"
"time"
)
const otherWord = "*"
var transforms = []string{
otherWord,
otherWord + "app",
otherWord + "site",
otherWord + "time",
"get" + otherWord,
"go" + otherWord,
"lets " + otherWord,
otherWord + "hq",
}
func main() {
rand.Seed(time.Now().UTC().UnixNano())
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
t := transforms[rand.Intn(len(transforms))]
fmt.Println(strings.Replace(t, otherWord, s.Text(), -1))
}
}

import
Appendix

main
otherWord

otherWord+"extra"
“ ”

transforms

app lets
main

math/rand

bufio.Scanner bufio.NewScanner
os.Stdin

bufio.Scanner io.Reader

io.Reader

bufio.ScanWords

Scan
bool
for
Scan true for
Scan false
Bytes Text
[]byte

for rand.Intn
transforms strings.Replace
otherWord fmt.Println
math/rand

crypto/rand

chat

chat

Scan false

echo

echo

transformations
sprinkle
domainify main.go

package main
var tlds = []string{"com", "net"}
const allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789_-"
func main() {
rand.Seed(time.Now().UTC().UnixNano())
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
text := strings.ToLower(s.Text())
var newText []rune
for _, r := range text {
if unicode.IsSpace(r) {
r = '-'
}
if !strings.ContainsRune(allowedChars, r) {
continue
}
newText = append(newText, r)
}
fmt.Println(string(newText) + "." +
tlds[rand.Intn(len(tlds))])
}
}

rand.Seed NewScanner os.Stdin

rune
newText rune allowedChars
strings.ContainsRune rune
unicode.IsSpace
rune
int32
h
ttp://blog.golang.org/strings

newText []rune .com .net


fmt.Println

domainify

“ ”

One (two) three! one-two-three.com

$GOPATH/src sprinkle
domainify

sprinkle domainify
sprinkle domanify
chat

.com .net
chat
a
cht a chaat

coolify sprinkle domainify


main.go

package main
const (
duplicateVowel bool = true
removeVowel bool = false
)
func randBool() bool {
return rand.Intn(2) == 0
}
func main() {
rand.Seed(time.Now().UTC().UnixNano())
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
word := []byte(s.Text())
if randBool() {
var vI int = -1
for i, char := range word {
switch char {
case 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U':
if randBool() {
vI = i
}
}
}
if vI >= 0 {
switch randBool() {
case duplicateVowel:
word = append(word[:vI+1], word[vI:]...)
case removeVowel:
word = append(word[:vI], word[vI+1:]...)
}
}
}
fmt.Println(string(word))
}
}

duplicateVowel removeVowel
switch
true false

randBool true
false rand
0 1
true

main
rand.Seed
randBool

randBool
true vI

randBool

switch randBool() {
case true:
word = append(word[:vI+1], word[vI:]...)
case false:
word = append(word[:vI], word[vI+1:]...) }

true false
duplicateVowel removeVowel
randBool
append
switch
[]byte append

blueprints
e vI 3

word[:vI+1]
+1

word[vI:]

word[:vI]

word[vI+1:]

fmt.Println
blueprints

cd

http://bighugelabs.com/
GET
https://github.
com/matryer/goblueprints

http://words.bighugelabs.com/

const

BHT_APIKEY

~/.bashrc
export
export BHT_APIKEY=abc123def456ghi789jkl

love

{
"noun":{
"syn":[
"passion",
"beloved",
"dear"
]
},
"verb":{
"syn":[
"love",
"roll in the hay",
"make out"
],
"ant":[
"hate"
]
}
}

syn ant

encoding/json

thesaurus $GOPATH/src
bighuge.go

package thesaurus
import (
"encoding/json"
"errors"
"net/http"
)
type BigHuge struct {
APIKey string
}
type synonyms struct {
Noun *words `json:"noun"`
Verb *words `json:"verb"`
}
type words struct {
Syn []string `json:"syn"`
}
func (b *BigHuge) Synonyms(term string) ([]string, error) {
var syns []string
response, err := http.Get("http://words.bighugelabs.com/api/2/" +
b.APIKey + "/" + term + "/json")
if err != nil {
return syns, errors.New("bighuge: Failed when looking for synonyms
for "" + term + """ + err.Error())
}
var data synonyms
defer response.Body.Close()
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
return syns, err
}
if data.Noun != nil {
syns = append(syns, data.Noun.Syn...)
}
if data.Verb != nil {
syns = append(syns, data.Verb.Syn...)
}
return syns, nil
}

BigHuge
Synonyms

synonyms words

Syn
encoding/json

encoding/json

synonyms
words

Synonyms term http.Get

term
log.Fatalln
1
io.Reader
json.NewDecoder data
synonyms
append noun verb
syns

BigHuge
Thesaurus thesaurus
thesaurus.go

package thesaurus
type Thesaurus interface {
Synonyms(term string) ([]string, error)
}

term

BigHuge
http://www.dictionary.com/

$GOPATH/src synonyms
main.go

func main() {
apiKey := os.Getenv("BHT_APIKEY")
thesaurus := &thesaurus.BigHuge{APIKey: apiKey}
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
word := s.Text()
syns, err := thesaurus.Synonyms(word)
if err != nil {
log.Fatalln("Failed when looking for synonyms for "+word+", err)
}
if len(syns) == 0 {
log.Fatalln("Couldn't find any synonyms for " + word + ")
}
for _, syn := range syns {
fmt.Println(syn)
}
}
}
main BHT_APIKEY
os.Getenv

os.Stdin Synonyms

chat
synonyms
domainify

chat

http://tool
s.ietf.org/html/rfc3912

No match
available main.go

func exists(domain string) (bool, error) {


const whoisServer string = "com.whois-servers.net"
conn, err := net.Dial("tcp", whoisServer+":43")
if err != nil {
return false, err
}
defer conn.Close()
conn.Write([]byte(domain + "rn"))
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
if strings.Contains(strings.ToLower(scanner.Text()), "no match") {
return false, nil
}
}
return true, nil
}

exists
43 whoisServer net.Dial

Close() conn
rn

“ ”
exists

bufio.Scanner
NewScanner net.Conn io.Reader
strings.ToLower
strings.Contains no match
false true
com.whois-servers.net .com .net

main exists

Yes No

main.go

var marks = map[bool]string{true: " ", false: " "}


func main() {
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
domain := s.Text()
fmt.Print(domain, " ")
exist, err := exists(domain)
if err != nil {
log.Fatalln(err)
}
fmt.Println(marks[!exist])
time.Sleep(1 * time.Second)
}
}

main
os.Stdin fmt.Print
fmt.Println exists
fmt.Println

time.Sleep
marks bool exists

fmt.Println(marks[!exist])
chat

chat synonyms

sprinkle

coolify
domainify

available

os/exec

domainfinder
lib lib

lib

build.sh build.bat

#!/bin/bash
echo Building domainfinder...
go build -o domainfinder
echo Building synonyms...
cd ../synonyms
go build -o ../domainfinder/lib/synonyms
echo Building available...
cd ../available
go build -o ../domainfinder/lib/available
cd ../build
echo Building sprinkle...
cd ../sprinkle
go build -o ../domainfinder/lib/sprinkle
cd ../build
echo Building coolify...
cd ../coolify
go build -o ../domainfinder/lib/coolify
cd ../build
echo Building domainify...
cd ../domainify
go build -o ../domainfinder/lib/domainify
cd ../build
echo Done.

domainfinder
go build lib
chmod +x build.sh
lib

no buildable Go source files


domainfinder
.go

main.go domainfinder

package main
var cmdChain = []*exec.Cmd{
exec.Command("lib/synonyms"),
exec.Command("lib/sprinkle"),
exec.Command("lib/coolify"),
exec.Command("lib/domainify"),
exec.Command("lib/available"),
}
func main() {
cmdChain[0].Stdin = os.Stdin
cmdChain[len(cmdChain)-1].Stdout = os.Stdout
for i := 0; i < len(cmdChain)-1; i++ {
thisCmd := cmdChain[i]
nextCmd := cmdChain[i+1]
stdout, err := thisCmd.StdoutPipe()
if err != nil {
log.Fatalln(err)
}
nextCmd.Stdin = stdout
}
for _, cmd := range cmdChain {
if err := cmd.Start(); err != nil {
log.Fatalln(err)
} else {
defer cmd.Process.Kill()
}
}
for _, cmd := range cmdChain {
if err := cmd.Wait(); err != nil {
log.Fatalln(err)
}
}
}

os/exec
cmdChain *exec.Cmd

main Stdin
os.Stdin Stdout
os.Stdout

Stdin Stdout

Stdin domainfinder

Stdout domainfinder
Start
Run

log.Fatalln

main domainfinder

domainfinder

build.sh build.bat domainfinder

clouds
synonyms

math/rand

domainfinder


go-nsq
twittervotes
counter

web

twittervotes
counter web

ballots
polls

{
"_id": "",
"title": "Poll title",
"options": ["one", "two", "three"],
"results": {
"one": 100,
"two": 200,
"three": 300
}
}

_id
options
results

https://github.com/matryer/g
oblueprints

mongod nsqd

counter
http://nsq.io/deployment/installing.html
install nsq

bin PATH

nsqlookupd

nsqlookupd nsqd
nsqlookupd
nsqd
nsqd

http://nsq.io/
go get

people

{"name":"Mat","lang":"en","points":57}
{"name":"Laurie","position":"Scrum Master"}
{"position":"Traditional Manager","exists":false}

mongod

http://www.mongodb.org/downloads
bin PATH

mongod
mgo http://labix.org/mgo

nsqlookupd nsqd
nsqd nsqlookupd
mongod

4160

--lookupd-tcp-address
nsqlookupd nsqd
nsqlookupd nsqd
dbpath
mongod

dbpath

$GOPATH/src
socialpoll
socialpoll
twittervotes main.go main
main

package main
func main(){}

twittervotes

mgo
options
Chapter 3 https://apps
.twitter.com SocialPoll

SP_TWITTER_KEY
SP_TWITTER_SECRET
SP_TWITTER_ACCESSTOKEN
SP_TWITTER_ACCESSSECRET

setup.sh setup.bat

setup.sh

#!/bin/bash
export SP_TWITTER_KEY=yC2EDnaNrEhN5fd33g...
export SP_TWITTER_SECRET=6n0rToIpskCo1ob...
export SP_TWITTER_ACCESSTOKEN=2427-13677...
export SP_TWITTER_ACCESSSECRET=SpnZf336u...

SET SP_TWITTER_KEY=yC2EDnaNrEhN5fd33g...
SET SP_TWITTER_SECRET=6n0rToIpskCo1ob...
SET SP_TWITTER_ACCESSTOKEN=2427-13677...
SET SP_TWITTER_ACCESSSECRET=SpnZf336u...
.bashrc C:\cmdauto.cmd

Setting environment variables on


Linux

net.Conn

dial http.Transport

twitter.go twittervotes

var conn net.Conn


func dial(netw, addr string) (net.Conn, error) {
if conn != nil {
conn.Close()
conn = nil
}
netc, err := net.DialTimeout(netw, addr, 5*time.Second)
if err != nil {
return nil, err
}
conn = netc
return netc, nil
}

dial conn
conn

io.ReadCloser
twitter.go

var reader io.ReadCloser


func closeConn() {
if conn != nil {
conn.Close()
}
if reader != nil {
reader.Close()
}
}

closeConn

closeConn

OAuth
twitter.go

var (
authClient *oauth.Client
creds *oauth.Credentials
)
func setupTwitterAuth() {
var ts struct {
ConsumerKey string `env:"SP_TWITTER_KEY,required"`
ConsumerSecret string `env:"SP_TWITTER_SECRET,required"`
AccessToken string `env:"SP_TWITTER_ACCESSTOKEN,required"`
AccessSecret string `env:"SP_TWITTER_ACCESSSECRET,required"`
}
if err := envdecode.Decode(&ts); err != nil {
log.Fatalln(err)
}
creds = &oauth.Credentials{
Token: ts.AccessToken,
Secret: ts.AccessSecret,
}
authClient = &oauth.Client{
Credentials: oauth.Credentials{
Token: ts.ConsumerKey,
Secret: ts.ConsumerSecret,
},
}
}
struct

ts
var ts struct... envdecode
go get
github.com/joeshaw/envdecode log
required

struct
envdecode
required

oauth.Credentials oauth.Client
go-oauth

twitter.go

var (
authSetupOnce sync.Once
httpClient *http.Client
)
func makeRequest(req *http.Request, params url.Values) (*http.Response,
error) {
authSetupOnce.Do(func() {
setupTwitterAuth()
httpClient = &http.Client{
Transport: &http.Transport{
Dial: dial,
},
}
})
formEnc := params.Encode()
req.Header.Set("Content-Type", "application/x-www-form- urlencoded")
req.Header.Set("Content-Length", strconv.Itoa(len(formEnc)))
req.Header.Set("Authorization", authClient.AuthorizationHeader(creds,
"POST",
req.URL, params))
return httpClient.Do(req)
}
sync.Once
makeRequest setupTwitterAuth
http.Client http.Transport dial

params

main.go dialdb closedb

var db *mgo.Session
func dialdb() error {
var err error
log.Println("dialing mongodb: localhost")
db, err = mgo.Dial("localhost")
return err
}
func closedb() {
db.Close()
log.Println("closed database connection")
}

mgo mgo.Session
db

loadOptions main.go

type poll struct {


Options []string
}
func loadOptions() ([]string, error) {
var options []string
iter := db.DB("ballots").C("polls").Find(nil).Iter()
var p poll
for iter.Next(&p) {
options = append(options, p.Options...)
}
iter.Close()
return options, iter.Err()
}

Options
poll db
polls ballots mgo
Find nil

mgo
query := col.Find(q).Sort("field").Limit(10).Skip(10)

Iter

poll All

append options

twittervotes

poll

twittervotes

append variadic

...

Err mgo.Iter
closeConn

twitter.go

type tweet struct {


Text string
}

twitter.go readFromTwitter
votes

func readFromTwitter(votes chan<- string) {


options, err := loadOptions()
if err != nil {
log.Println("failed to load options:", err)
return
}
u, err := url.Parse("https://stream.twitter.com/1.1/statuses
/filter.json")
if err != nil {
log.Println("creating filter request failed:", err)
return
}
query := make(url.Values)
query.Set("track", strings.Join(options, ","))
req, err := http.NewRequest("POST",u.String(),strings.NewReader
(query.Encode()))
if err != nil {
log.Println("creating filter request failed:", err)
return
}
resp, err := makeRequest(req, query)
if err != nil {
log.Println("making request failed:", err)
return
}
reader := resp.Body
decoder := json.NewDecoder(reader)
for {
var t tweet
if err := decoder.Decode(&t); err != nil {
break
}
for _, option := range options {
if strings.Contains(
strings.ToLower(t.Text),
strings.ToLower(option),
) {
log.Println("vote:", option)
votes <- option
}
}
}
}

loadOptions url.Parse url.URL


url.Values query
POST
url.Values makeRequest
json.Decoder
for Decode

t
Text
votes

votes
chan<- string
chan<-
<-chan

readFromTwitter
Decode

readFromTwitter votes

struct{}

struct{}
struct{}{}
bool true
false

http://play.golang.org
bool
fmt.Println(reflect.TypeOf(true).Size()) = 1
struct{}{}
fmt.Println(reflect.TypeOf(struct{}{}).Size()) = 0
twitter.go

func startTwitterStream(stopchan <-chan struct{}, votes chan<- string) <-


chan struct{} {
stoppedchan := make(chan struct{}, 1)
go func() {
defer func() {
stoppedchan <- struct{}{}
}()
for {
select {
case <-stopchan:
log.Println("stopping Twitter...")
return
default:
log.Println("Querying Twitter...")
readFromTwitter(votes)
log.Println(" (waiting)")
time.Sleep(10 * time.Second) // wait before
reconnecting
}
}
}()
return stoppedchan
}

stopchan <-chan
struct{}

votes
<-chan struct{}
startTwitterStream stoppedchan
struct{}{}
stoppedchan

for
stopchan
stoppedchan
readFromTwitter votes

time.Sleep
stopchan

votes

twittervotes

publishVotes votes
<-chan string

votes chan<-
string <-chan string

make(chan string)
<-
votes

publishVotes main.go

func publishVotes(votes <-chan string) <-chan struct{} {


stopchan := make(chan struct{}, 1)
pub, _ := nsq.NewProducer("localhost:4150",
nsq.NewConfig())
go func() {
for vote := range votes {
pub.Publish("votes", []byte(vote)) // publish vote
}
log.Println("Publisher: Stopping")
pub.Stop()
log.Println("Publisher: Stopped")
stopchan <- struct{}{}
}()
return stopchan
}

stopchan
struct{}{} stopchan

stopchan

stopchan

NewProducer
localhost

votes for...range

votes for
votes
stopchan
publishVotes

main

main

var stoplock sync.Mutex // protects stop


stop := false
stopChan := make(chan struct{}, 1)
signalChan := make(chan os.Signal, 1)
go func() {
<-signalChan
stoplock.Lock()
stop = true
stoplock.Unlock()
log.Println("Stopping...")
stopChan <- struct{}{}
closeConn()
}()
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
bool sync.Mutex

stopChan signalChan signal.Notify


signalChan SIGINT
SIGTERM stopChan

startTwitterStream

signalChan <-

stop true

if err := dialdb(); err != nil {


log.Fatalln("failed to dial MongoDB:", err)
}
defer closedb()

readFromTwitter

closeConn
readFromTwitter
main

// start things
votes := make(chan string) // chan for votes
publisherStoppedChan := publishVotes(votes)
twitterStoppedChan := startTwitterStream(stopChan, votes)
go func() {
for {
time.Sleep(1 * time.Minute)
closeConn()
stoplock.Lock()
if stop {
stoplock.Unlock()
return
}
stoplock.Unlock()
}
}()
<-twitterStoppedChan
close(votes)
<-publisherStoppedChan

votes
chan<- <-
chan
publishVotes votes
publisherStoppedChan
startTwitterStream stopChan main
votes
twitterStoppedChan

for
closeConn bool

stoplock

twitterStoppedChan
stopChan
votes for...range
publisherStoppedChan

twittervotes

mongo
polls ballots

results
counter
results

nsq_tail
twittervotes

twittervotes

nsq_tail

counter

counter twittervotes
main.go

package main
import (
"flag"
"fmt"
"os"
)
var fatalErr error
func fatal(e error) {
fmt.Println(e)
flag.PrintDefaults()
fatalErr = e
}
func main() {
defer func() {
if fatalErr != nil {
os.Exit(1)
}
}()
}

log.Fatal
os.Exit

fatal

os.Exit(1)
1

main

defer

log.Println("Connecting to database...")
db, err := mgo.Dial("localhost")
if err != nil {
fatal(err)
return
}
defer func() {
log.Println("Closing database connection...")
db.Close()
}()
pollData := db.DB("ballots").C("polls")

mgo.Dial

mgo
ballots.polls pollData

votes
main

var counts map[string]int


var countsLock sync.Mutex

sync.Mutex

main

log.Println("Connecting to nsq...")
q, err := nsq.NewConsumer("votes", "counter", nsq.NewConfig())
if err != nil {
fatal(err)
return
}
NewConsumer votes
twittervotes
NewConsumer fatal

q.AddHandler(nsq.HandlerFunc(func(m *nsq.Message) error {


countsLock.Lock()
defer countsLock.Unlock()
if counts == nil {
counts = make(map[string]int)
}
vote := string(m.Body)
counts[vote]++
return nil
}))

AddHandler nsq.Consumer
votes

countsLock

NewConsumer
Lock

Unlock Lock Unlock

counts nil

int
nil

if err := q.ConnectToNSQLookupd("localhost:4161");
err !=nil {
fatal(err)
return
}
nsqlookupd

doCount

func doCount(countsLock *sync.Mutex, counts *map[string]int, pollData


*mgo.Collection) {
countsLock.Lock()
defer countsLock.Unlock()
if len(*counts) == 0 {
log.Println("No new votes, skipping database update")
return
}
log.Println("Updating database...")
log.Println(*counts)
ok := true
for option, count := range *counts {
sel := bson.M{"options": bson.M{"$in":
[]string{option}}}
up := bson.M{"$inc": bson.M{"results." +
option:count}}
if _, err := pollData.UpdateAll(sel, up); err != nil {
log.Println("failed to update:", err)
ok = false
}
}
if ok {
log.Println("Finished updating database...")
*counts = nil // reset counts
}
}

doCount countsLock
counts
*

*counts = nil
nil
counts

mgo mgo/bson
mgo bson bson.M

bson.M
map[string]interface{}

{
"options": {
"$in": ["happy"]
}
}

"happy"
options

{
"$inc": {
"results.happy": 3
}
}

results.happy
results
happy results

UpdateAll pollsData

Update
ok false counts
updateDuration

main

const updateDuration = 1 * time.Second

time.Ticker doCount
select

doCount main
twittervotes
main

ticker := time.NewTicker(updateDuration)
termChan := make(chan os.Signal, 1)
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM,syscall.SIGHUP)
for {
select {
case <-ticker.C:
doCount(&countsLock, &counts,pollData) case <- termChan:ticker.Stop()
q.Stop()
case <-q.StopChan:
// finished
return
}
}

time.Ticker C
updateDuration
select doCount termChan q.StopChan

termChan
select
termChan StopChan
termChan
time.Ticker

StopChan

nsqlookupd nsqd mongod

twittervotes
counter

counter
results

find pretty

results

mgo
bson.M
map[string]interface{}
nsqlookupd nsqd

twittervotes
counter

twittervotes

twittervotes


Chapter 5

http.HandlerFunc

context
GET
POST

GET /polls
GET /polls/{id}
POST /polls
DELETE /polls/{id}
{id}
context

http.Request context.Context
request.Context()
request.WithContext() http.Request
Context

context.WithValue

ctx := context.WithValue(r.Context(), "key", "value")

ctx

Handler.ServeHTTP(w, r.WithContext(ctx))

https://golang.org/pk
g/context/

interface{}

func WithValue(parent Context, key, val interface{}) Context


interface{}

struct

main.go api

package main
func main(){}

contextKey

type contextKey struct {


name string
}

name

var contextKeyAPIKey = &contextKey{"api-key"}

contextKey
contextKeyAPIKey contextKey
api-key

func APIKey(ctx context.Context) (string, bool) {


key, ok := ctx.Value(contextKeyAPIKey).(string)
return key, ok
}

context.Context ok
contextKey contextKeyAPIKey
APIKey main

Chapter 2
http.Handler

http.HandlerFunc

HandlerFunc withAPIKey
main.go

func withAPIKey(fn http.HandlerFunc) http.HandlerFunc {


return func(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
if !isValidAPIKey(key) {
respondErr(w, r, http.StatusUnauthorized, "invalid
API key")
return
}
ctx := context.WithValue(r.Context(),
contextKeyAPIKey, key)
fn(w, r.WithContext(ctx))
}
}
withAPIKey http.HandlerFunc

withAPIKey

http.HandlerFunc key
isValidAPIKey false
invalid API key
http.HandlerFunc
key
http.HandlerFunc
http.HandleFunc

isValidAPIKey

func isValidAPIKey(key string) bool {


return key == "abc123"
}

abc123
false

isValidAPIKey withAPIKey

Access-Control-Allow-Origin *
Location –

Access-Control-Expose-Headers main.go

func withCORS(fn http.HandlerFunc) http.HandlerFunc {


return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Expose-Headers",
"Location")
fn(w, r)
}
}

ResponseWriter http.HandlerFunc

https://github.com/faster
ness/cors

main.go

Server

// Server is the API server.


type Server struct {
db *mgo.Session
}

– net/http
respond.go

func decodeBody(r *http.Request, v interface{}) error {


defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(v)
}
func encodeBody(w http.ResponseWriter, r *http.Request, v interface{})
error {
return json.NewEncoder(w).Encode(v)
}

Request
ResponseWriter

respond.go

func respond(w http.ResponseWriter, r *http.Request,


status int, data interface{}) {
w.WriteHeader(status)
if data != nil {
encodeBody(w, r, data)
}
}

ResponseWriter
encodeBody

respondErr

func respondErr(w http.ResponseWriter, r *http.Request,


status int, args ...interface{}) {
respond(w, r, status, map[string]interface{}{
"error": map[string]interface{}{
"message": fmt.Sprint(args...),
},
})
}

respond
error

http.StatusText

func respondHTTPErr(w http.ResponseWriter, r *http.Request, status int) {


respondErr(w, r, status, http.StatusText(status))
}

http.Request
net/http

/people/1/books/2 http.Request
URL.Path
1 2

mux

/users/{userID}/comments/{commentID}
/users/1/comments/2

path.go

package main
import (
"strings"
)
const PathSeparator = "/"
type Path struct {
Path string
ID string
}
func NewPath(p string) *Path {
var id string
p = strings.Trim(p, PathSeparator)
s := strings.Split(p, PathSeparator)
if len(s) > 1 {
id = s[len(s)-1]
p = strings.Join(s[:len(s)-1], PathSeparator)
}
return &Path{Path: p, ID: id}
}
func (p *Path) HasID() bool {
return len(p.ID) > 0
}
NewPath
Path
strings.Trim strings.Split
PathSeparator
len(s) > 1
s[len(s)-1]
s[:len(s)-1]
PathSeparator

collection/id
Path

/ / nil false
/people/ people nil false
/people/1/ people 1 true

main

main
main.go main

func main() {
var (
addr = flag.String("addr", ":8080", "endpoint
address")
mongo = flag.String("mongo", "localhost", "mongodb
address")
)
log.Println("Dialing mongo", *mongo)
db, err := mgo.Dial(*mongo)
if err != nil {
log.Fatalln("failed to connect to mongo:", err)
}
defer db.Close()
s := &Server{
db: db,
}
mux := http.NewServeMux()
mux.HandleFunc("/polls/",
withCORS(withAPIKey(s.handlePolls)))
log.Println("Starting web server on", *addr)
http.ListenAndServe(":8080", mux)
log.Println("Stopping...")
}

main
addr mongo flag

log.Fatalln
db

s
http.ServeMux

/polls/ handlePolls

HandleFunc ServeMux

withCORS(withAPIKey(handlePolls))

http.HandlerFunc

/polls/

withCORS
withAPIKey

handlePolls
respond.go
withAPIKey
withCORS

handlePolls

polls.go

package main
import "gopkg.in/mgo.v2/bson"
type poll struct {
ID bson.ObjectId `bson:"_id" json:"id"`
Title string `json:"title"`
Options []string `json:"options"`
Results map[string]int `json:"results,omitempty"`
APIKey string `json:"apikey"`
}

poll

APIKey

ID

struct

reflect
bson json
encoding/json
gopkg.in/mgo.v2/bson

struct

ID bson.ObjectId `bson:"_id" json:"id"`

ID id _id

GET /polls/ POST

switch
polls.go handlePolls

func (s *Server) handlePolls(w http.ResponseWriter,


r *http.Request) {
switch r.Method {
case "GET":
s.handlePollsGet(w, r)
return
case "POST":
s.handlePollsPost(w, r)
return
case "DELETE":
s.handlePollsDelete(w, r)
return
}
// not found
respondHTTPErr(w, r, http.StatusNotFound)
}

GET POST DELETE 404


http.StatusNotFound
handlePolls

func (s *Server) handlePollsGet(w http.ResponseWriter,


r *http.Request) {
respondErr(w, r, http.StatusInternalServerError,
errors.New("not
implemented"))
}
func (s *Server) handlePollsPost(w http.ResponseWriter,
r *http.Request) {
respondErr(w, r, http.StatusInternalServerError,
errors.New("not
implemented"))
}
func (s *Server) handlePollsDelete(w http.ResponseWriter,
r *http.Request) {
respondErr(w, r, http.StatusInternalServerError,
errors.New("not
implemented"))
}

mux
func (s *Server) handlePollsGet(w http.ResponseWriter,
r *http.Request) {
session := s.db.Copy()
defer session.Close()
c := session.DB("ballots").C("polls")
var q *mgo.Query
p := NewPath(r.URL.Path)
if p.HasID() {
// get specific poll
q = c.FindId(bson.ObjectIdHex(p.ID))
} else {
// get all polls
q = c.Find(nil)
}
var result []*poll
if err := q.All(&result); err != nil {
respondErr(w, r, http.StatusInternalServerError, err)
return
}
respond(w, r, http.StatusOK, &result)
}

mgo
polls –

mgo.Query
FindId polls nil Find

bson.ObjectId ObjectIdHex

All poll
[]*poll All
mgo result
Iter
Limit Skip

polls

mongo

api

GET /polls/
http://localhost:8080/polls/key=abc123

http://localhost:8080/polls/5415b060a02cd4adb487c3aekey=abc123
mgo.Query

POST /polls/
POST

func (s *Server) handlePollsPost(w http.ResponseWriter,


r *http.Request) {
session := s.db.Copy()
defer session.Close()
c := session.DB("ballots").C("polls")
var p poll
if err := decodeBody(r, &p); err != nil {
respondErr(w, r, http.StatusBadRequest, "failed to
read poll from request", err)
return
}
apikey, ok := APIKey(r.Context())
if ok {
p.APIKey = apikey
}
p.ID = bson.NewObjectId()
if err := c.Insert(p); err != nil {
respondErr(w, r, http.StatusInternalServerError,
"failed to insert
poll", err)
return
}
w.Header().Set("Location", "polls/"+p.ID.Hex())
respond(w, r, http.StatusCreated, nil)
}
respondErr

mgo Insert
Location 201
http.StatusCreated

DELETE
/polls/5415b060a02cd4adb487c3ae
200 Success

func (s *Server) handlePollsDelete(w http.ResponseWriter,


r *http.Request) {
session := s.db.Copy()
defer session.Close()
c := session.DB("ballots").C("polls")
p := NewPath(r.URL.Path)
if !p.HasID() {
respondErr(w, r, http.StatusMethodNotAllowed,
"Cannot delete all polls.")
return
}
if err := c.RemoveId(bson.ObjectIdHex(p.ID)); err != nil {
respondErr(w, r, http.StatusInternalServerError,
"failed to delete poll", err)
return
}
respond(w, r, http.StatusOK, nil) // ok
}
GET

StatusMethodNotAllowed
RemoveId
bson.ObjectId
http.StatusOK

DELETE
DELETE
OPTIONS
DELETE Access-Control-Request-Method

switch OPTIONS

case "OPTIONS":
w.Header().Add("Access-Control-Allow-Methods", "DELETE")
respond(w, r, http.StatusOK, nil)
return

DELETE
Access-Control-Allow-Methods DELETE *
withCORS
Access-Control-Allow-Methods
DELETE

http://enable
-cors.org/
http://curl.haxx.se/dlwiz/type=bin

api

curl -X
GET
DELETE

index.html
view.html
new.html

web api
main.go

package main
import (
"flag"
"log"
"net/http"
)
func main() {
var addr = flag.String("addr", ":8081", "website address")
flag.Parse()
mux := http.NewServeMux()
mux.Handle("/", http.StripPrefix("/",
http.FileServer(http.Dir("public"))))
log.Println("Serving website at:", *addr)
http.ListenAndServe(*addr, mux)
}

addr http.ServeMux
public

– –

https://github.com/
matryer/goblueprints

public web index.html

<!DOCTYPE html>
<html>
<head>
<title>Polls</title>
<link rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/
bootstrap.min.css">
</head>
<body>
</body>
</html>

body

<div class="container">
<div class="col-md-4"></div>
<div class="col-md-4">
<h1>Polls</h1>
<ul id="polls"></ul>
<a href="new.html" class="btn btn-primary">Create new poll</a>
</div>
<div class="col-md-4"></div>
</div>

new.html

script

<script
src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script>
$(function(){
var update = function(){
$.get("http://localhost:8080/polls/key=abc123", null, null, "json")
.done(function(polls){
$("#polls").empty();
for (var p in polls) {
var poll = polls[p];
$("#polls").append(
$("<li>").append(
$("<a>")
.attr("href", "view.htmlpoll=polls/" + poll.id)
.text(poll.title)
)
)
}
}
);
window.setTimeout(update, 10000);
}
update();
});
</script>

$.get
– –

view.html
new.html public

<!DOCTYPE html>
<html>
<head>
<title>Create Poll</title>
<link rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/
bootstrap.min.css">
</head>
<body>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">
</script>
</body>
</html>

body

<div class="container">
<div class="col-md-4"></div>
<form id="poll" role="form" class="col-md-4">
<h2>Create Poll</h2>
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" id="title"
placeholder="Title">
</div>
<div class="form-group">
<label for="options">Options</label>
<input type="text" class="form-control" id="options"
placeholder="Options">
<p class="help-block">Comma separated</p>
</div>
<button type="submit" class="btn btn-primary">
Create Poll</button> or <a href="/">cancel</a>
</form>
<div class="col-md-4"></div>
</div>
script

<script>
$(function(){
var form = $("form#poll");
form.submit(function(e){
e.preventDefault();
var title = form.find("input[id='title']").val();
var options = form.find("input[id='options']").val();
options = options.split(",");
for (var opt in options) {
options[opt] = options[opt].trim();
}
$.post("http://localhost:8080/polls/key=abc123",
JSON.stringify({
title: title, options: options
})
).done(function(d, s, r){
location.href = "view.htmlpoll=" +
r.getResponseHeader("Location");
});
});
});
</script>

submit val

$.post POST
JSON.stringify

Location view.html

view.html
view.html public

<!DOCTYPE html>
<html>
<head>
<title>View Poll</title>
<link rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="col-md-4"></div>
<div class="col-md-4">
<h1 data-field="title">...</h1>
<ul id="options"></ul>
<div id="chart"></div>
<div>
<button class="btn btn-sm" id="delete">Delete this poll</button>
</div>
</div>
<div class="col-md-4"></div>
</div>
</body>
</html>

div view.html
body script

<script src="//www.google.com/jsapi"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">
</script>
<script>
google.load('visualization', '1.0', {'packages':['corechart']});
google.setOnLoadCallback(function(){
$(function(){
var chart;
var poll = location.href.split("poll=")[1];
var update = function(){
$.get("http://localhost:8080/"+poll+"key=abc123", null, null,
"json")
.done(function(polls){
var poll = polls[0];
$('[data-field="title"]').text(poll.title);
$("#options").empty();
for (var o in poll.results) {
$("#options").append(
$("<li>").append(
$("<small>").addClass("label label
default").text(poll.results[o]),
" ", o
)
)
}
if (poll.results) {
var data = new google.visualization.DataTable();
data.addColumn("string","Option");
data.addColumn("number","Votes");
for (var o in poll.results) {
data.addRow([o, poll.results[o]])
}
if (!chart) {
chart = new google.visualization.PieChart
(document.getElementById('chart'));
}
chart.draw(data, {is3D: true});
}
}
);
window.setTimeout(update, 1000);
};
update();
$("#delete").click(function(){
if (confirm("Sure")) {
$.ajax({
url:"http://localhost:8080/"+poll+"key=abc123",
type:"DELETE"
})
.done(function(){
location.href = "/";
})
}
});
});
});
</script>

poll= update

window.setTimeout
update $.get GET
/polls/{id} {id}

results
google.visualization.PieChart
google.visualization.DataTable draw

setTimeout update

click delete
DELETE
OPTIONS

handlePolls

api counter twittervotes web

nsqlookupd

nsqd

counter
twittervotes

api

web

http://localhost:8081/
Moods
happy,sad,fail,success
bitballoon.com


encoding/json –


http.Get

math/rand

http.Request
bar cafe movie_theater math/rand

https://de
velopers.google.com/places/documentation/supported_types

GET /journeys

[
{
name: "Romantic",
journey: "park|bar|movie_theater|restaurant|florist"
},
{
name: "Shopping",
journey: "department_store|clothing_store|jewelry_store"
}
]

name
journey

GET /recommendations
lat=1&lng=2&journey=bar|cafe&radius=10&cost=$...$$$$$
lat lng

radius

cost

$
$$$$$ $...$$
$$$$...$$$$$

[
{
icon: "http://maps.gstatic.com/mapfiles/place_api/icons/cafe-
71.png",
lat: 51.519583, lng: -0.146251,
vicinity: "63 New Cavendish St, London",
name: "Asia House",
photos: [{
url: "https://maps.googleapis.com/maps/api/place/photo
maxwidth=400&photoreference=CnRnAAAAyLRN"
}]
}, ...
]

lat lng
name vicinity
photos vicinity
icon
meander GOPATH journeys.go

package meander
type j struct {
Name string
PlaceTypes []string
}
var Journeys = []interface{}{
j{Name: "Romantic", PlaceTypes: []string{"park", "bar",
"movie_theater", "restaurant", "florist", "taxi_stand"}},
j{Name: "Shopping", PlaceTypes: []string{"department_store", "cafe",
"clothing_store", "jewelry_store", "shoe_store"}},
j{Name: "Night Out", PlaceTypes: []string{"bar", "casino", "food",
"bar", "night_club", "bar", "bar", "hospital"}},
j{Name: "Culture", PlaceTypes: []string{"museum", "cafe", "cemetery",
"library", "art_gallery"}},
j{Name: "Pamper", PlaceTypes: []string{"hair_care", "beauty_salon",
"cafe", "spa"}},
}

j meander
Journeys

golint
golint

golint
https://github.com/golang/lint

[]interface{}
meander main

meander/cmd/meander
meander

meander
main cmd
meander
cmd meander

cmd/meander main.go

package main
func main() {
//meander.APIKey = "TODO"
http.HandleFunc("/journeys", func(w http.ResponseWriter,
r *http.Request) {
respond(w, r, meander.Journeys)
})
http.ListenAndServe(":8080", http.DefaultServeMux)
}
func respond(w http.ResponseWriter, r *http.Request, data []interface{})
error {
return json.NewEncoder(w).Encode(data)
}

/journeys

encoding/json net/http runtime


meander
APIKey meander
HandleFunc
net/http
meander.Journeys
respond
http.ResponseWriter

cmd/meander
go run

http://localhost:8080/journeys Journeys

[{
Name: "Romantic",
PlaceTypes: [
"park",
"bar",
"movie_theater",
"restaurant",
"florist",
"taxi_stand"
]
}, ...]

PlaceTypes Types
journey meander
public.go

package meander
type Facade interface {
Public() interface{}
}
func Public(o interface{}) interface{} {
if p, ok := o.(Facade); ok {
return p.Public()
}
return o
}

Facade Public
Public
Facade Public() interface{}

Public
ResponseWriter

Facade
Reader Writer
Publicer

Public j
journeys.go

func (j j) Public() interface{} {


return map[string]interface{}{
"name": j.Name,
"journey": strings.Join(j.PlaceTypes, "|"),
}
}

j PlaceTypes
cmd/meander/main.go respond
Public

func respond(w http.ResponseWriter, r *http.Request, data []interface{})


error {
publicData := make([]interface{}, len(data))
for i, d := range data {
publicData[i] = meander.Public(d)
}
return json.NewEncoder(w).Encode(publicData)
}

meander.Public
j Public

cmd/meander go run main.go


http://localhost:8080/journeys

[{
journey: "park|bar|movie_theater|restaurant|florist|taxi_stand",
name: "Romantic"
}, ...]

[]string MarshalJSON

Facade Public

meander
query.go

package meander
type Place struct {
*googleGeometry `json:"geometry"`
Name string `json:"name"`
Icon string `json:"icon"`
Photos []*googlePhoto `json:"photos"`
Vicinity string `json:"vicinity"`
}
type googleResponse struct {
Results []*Place `json:"results"`
}
type googleGeometry struct {
*googleLocation `json:"location"`
}
type googleLocation struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
}
type googlePhoto struct {
PhotoRef string `json:"photo_reference"`
URL string `json:"url"`
}

http://developers.google.com/pla
ces/documentation/search

Place
googleGeometry
googleLocation
googleGeometry Lat Lng
Place

Place
Public

func (p *Place) Public() interface{} {


return map[string]interface{}{
"name": p.Name,
"icon": p.Icon,
"photos": p.Photos,
"vicinity": p.Vicinity,
"lat": p.Lat,
"lng": p.Lng,
}
}
golint

meander
query.go

var APIKey string

main.go // APIKey
TODO

iota const
String

ParseType

cost_level.go meander

package meander
type Cost int8
const (
_ Cost = iota
Cost1
Cost2
Cost3
Cost4
Cost5
)

Cost
int8
iota
Cost
Cost

iota
iota

String
Cost

fmt.Println

String()
Stringer GoStringer fmt http://gola
ng.org/pkg/fmt/#Stringer
cost_level.go cost_level_test.go

package meander_test
import (
"testing"
"github.com/cheekybits/is"
"path/to/meander"
)
func TestCostValues(t *testing.T) {
is := is.New(t)
is.Equal(int(meander.Cost1), 1)
is.Equal(int(meander.Cost2), 2)
is.Equal(int(meander.Cost3), 3)
is.Equal(int(meander.Cost4), 4)
is.Equal(int(meander.Cost5), 5)
}

go get is https://git
hub.com/cheekybits/is

is

meander
meander meander_test

meander
go test

Cost
cost_level_test.go

func TestCostString(t *testing.T) {


is := is.New(t)
is.Equal(meander.Cost1.String(), "$")
is.Equal(meander.Cost2.String(), "$$")
is.Equal(meander.Cost3.String(), "$$$")
is.Equal(meander.Cost4.String(), "$$$$")
is.Equal(meander.Cost5.String(), "$$$$$")
}

String
String

Cost String

var costStrings = map[string]Cost{


"$": Cost1,
"$$": Cost2,
"$$$": Cost3,
"$$$$": Cost4,
"$$$$$": Cost5,
}
func (l Cost) String() string {
for s, v := range costStrings {
if l == v {
return s
}
}
return "invalid"
}

map[string]Cost
String
strings.Repeat("$", int(l))

Cost3 $$$

ParseCost

cost_value_test.go

func TestParseCost(t *testing.T) {


is := is.New(t)
is.Equal(meander.Cost1, meander.ParseCost("$"))
is.Equal(meander.Cost2, meander.ParseCost("$$"))
is.Equal(meander.Cost3, meander.ParseCost("$$$"))
is.Equal(meander.Cost4, meander.ParseCost("$$$$"))
is.Equal(meander.Cost5, meander.ParseCost("$$$$$"))
}

ParseCost

cost_value.go

func ParseCost(s string) Cost {


return costStrings[s]
}

Cost

CostRange
cost_value_test.go

func TestParseCostRange(t *testing.T) {


is := is.New(t)
var l meander.CostRange
var err error
l, err = meander.ParseCostRange("$$...$$$")
is.NoErr(err)
is.Equal(l.From, meander.Cost2)
is.Equal(l.To, meander.Cost3)
l, err = meander.ParseCostRange("$...$$$$$")
is.NoErr(err)
is.Equal(l.From, meander.Cost1)
is.Equal(l.To, meander.Cost5)
}
func TestCostRangeString(t *testing.T) {
is := is.New(t)
r := meander.CostRange{
From: meander.Cost2,
To: meander.Cost4,
}
is.Equal("$$...$$$$", r.String())
}

meander.CostRange
From meander.Cost2 To meander.Cost3 is.NoErr

CostRange.String

CostRange String
ParseString

type CostRange struct {


From Cost
To Cost
}
func (r CostRange) String() string {
return r.From.String() + "..." + r.To.String()
}
func ParseCostRange(s string) (CostRange, error) {
var r CostRange
segs := strings.Split(s, "...")
if len(segs) != 2 {
return r, errors.New("invalid cost range")
}
r.From = ParseCost(segs[0])
r.To = ParseCost(segs[1])
return r, nil
}

$...$$$$$ Cost
From To
query.go

type Query struct {


Lat float64
Lng float64
Journey []string
Radius int
CostRangeStr string
}

find

func (q *Query) find(types string) (*googleResponse, error) {


u := "https://maps.googleapis.com/maps/api/place/nearbysearch/json"
vals := make(url.Values)
vals.Set("location", fmt.Sprintf("%g,%g", q.Lat, q.Lng))
vals.Set("radius", fmt.Sprintf("%d", q.Radius))
vals.Set("types", types)
vals.Set("key", APIKey)
if len(q.CostRangeStr) > 0 {
r, err := ParseCostRange(q.CostRangeStr)
if err != nil {
return nil, err
}
vals.Set("minprice", fmt.Sprintf("%d", int(r.From)-1))
vals.Set("maxprice", fmt.Sprintf("%d", int(r.To)-1))
}
res, err := http.Get(u + "" + vals.Encode())
if err != nil {
return nil, err
}
defer res.Body.Close()
var response googleResponse
if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
return nil, err
}
return &response, nil
}
url.Values lat lng radius APIKey

url.Values map[string][]string
make new

types
CostRangeStr minprice maxprice
http.Get
json.Decoder
googleResponse

find Run
Query

// Run runs the query concurrently, and returns the results.


func (q *Query) Run() []interface{} {
rand.Seed(time.Now().UnixNano())
var w sync.WaitGroup
var l sync.Mutex
places := make([]interface{}, len(q.Journey))
for i, r := range q.Journey {
w.Add(1)
go func(types string, i int) {
defer w.Done()
response, err := q.find(types)
if err != nil {
log.Println("Failed to find places:", err)
return
}
if len(response.Results) == 0 {
log.Println("No places found for", types)
return
}
for _, result := range response.Results {
for _, photo := range result.Photos {
photo.URL =
"https://maps.googleapis.com/maps/api/place/photo" +
"maxwidth=1000&photoreference=" + photo.PhotoRef + "&key="
+ APIKey
}
}
randI := rand.Intn(len(response.Results))
l.Lock()
places[i] = response.Results[randI]
l.Unlock()
}(r, i)
}
w.Wait() // wait for everything to finish
return places
}

Run rand

Query.find sync.WaitGroup
sync.Mutex

Journey bar cafe


movie_theater 1 WaitGroup
w.Done WaitGroup
find

photoreference

rand.Intn
places
sync.Mutex

w.Wait
/recommendations main.go
cmd/meander main

http.HandleFunc("/recommendations", cors(func(w
http.ResponseWriter, r *http.Request) {
q := &meander.Query{
Journey: strings.Split(r.URL.Query().Get("journey"), "|"),
}
var err error
q.Lat, err = strconv.ParseFloat(r.URL.Query().Get("lat"), 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
q.Lng, err = strconv.ParseFloat(r.URL.Query().Get("lng"), 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
q.Radius, err = strconv.Atoi(r.URL.Query().Get("radius"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
q.CostRangeStr = r.URL.Query().Get("cost")
places := q.Run()
respond(w, r, places)
}))

meander.Query Run
http.Request
Query Get

bar|cafe|movie_theater
strconv

http.Error http.StatusBadRequest
Access-Control-Allow-Origin *
http.HandlerFunc

cmd/meander

main.go cors

func cors(f http.HandlerFunc) http.HandlerFunc {


return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
f(w, r)
}
}

http.HandlerFunc

cors
main

func main() {
meander.APIKey = "YOUR_API_KEY"
http.HandleFunc("/journeys", cors(func(w http.ResponseWriter,
r *http.Request)
{
respond(w, r, meander.Journeys)
}))
http.HandleFunc("/recommendations", cors(func(w http.ResponseWriter,
r *http.Request) {
q := &meander.Query{
Journey: strings.Split(r.URL.Query().Get("journey"), "|"),
}
var err error
q.Lat, err = strconv.ParseFloat(r.URL.Query().Get("lat"), 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
q.Lng, err = strconv.ParseFloat(r.URL.Query().Get("lng"), 64)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
q.Radius, err = strconv.Atoi(r.URL.Query().Get("radius"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
q.CostRangeStr = r.URL.Query().Get("cost")
places := q.Run()
respond(w, r, places)
}))
log.Println("serving meander API on :8080")
http.ListenAndServe(":8080", http.DefaultServeMux)
}

r.URL.Query()

cmd/meander meander
meander

http://mygeoposition.com/ x,y

51.520707 x 0.153809
40.7127840 x -74.0059410
35.6894870 x 139.6917060
37.7749290 x -122.4194160

/recommendations

http://localhost:8080/recommendations
lat=51.520707&lng=-0.153809&radius=5000&
journey=cafe|bar|casino|restaurant&
cost=$...$$$
https
://github.com/matryer/goblueprints/tree/master/chapter7/meanderweb
meanderweb GOPATH

meanderweb

localhost:8081
localhost:8080

http://localhost:8081/
Facade

iota

String

ioutil os

os

filepath.Walk

archive/zip
cmd cmds

gofmt goimports

backup

/backup - package
/backup/cmds/backup - user interaction tool
/backup/cmds/backupd - worker daemon
cmd
go install

cmd

backup

Archiver

GOPATH/src backup
archiver.go

package backup
type Archiver interface {
Archive(src, dest string) error
}

Archiver Archive

io
Archiver

Archiver

struct archiver.go

type zipper struct{}

// Zip is an Archiver that zips and unzips files.


var ZIP Archiver = (*zipper)(nil)

ZIP Archiver
Archiver
nil *zipper nil
zipper zipper

zipper

Archiver
zipper Archive

Archive

var _ Interface = (*Implementation)(nil)

Archive
zipper

archiver.go

func (z *zipper) Archive(src, dest string) error {


if err := os.MkdirAll(filepath.Dir(dest), 0777); err != nil {
return err
}
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
w := zip.NewWriter(out)
defer w.Close()
return filepath.Walk(src, func(path string, info os.FileInfo, err error)
error {
if info.IsDir() {
return nil // skip
}
if err != nil {
return err
}
in, err := os.Open(path)
if err != nil {
return err
}
defer in.Close()
f, err := w.Create(path)
if err != nil {
return err
}
_, err = io.Copy(f, in)
if err != nil {
return err
}
return nil
})
}

archive/zip
Archive

os.MkdirAll 0777

os.Create dest
defer
out.Close()
zip.NewWriter zip.Writer

zip.Writer filepath.Walk
src

filepath.Walk

filepath.Walk
filepath.WalkFunc

filepath.WalkFunc

filepath.WalkFunc func(path
string, info os.FileInfo, err error) error

filepath.Walk
os.FileInfo

SkipDir
filepath.Walk Archive
info.IsDir nil

Archive
filepath.Walk

os.Open

Create ZipWriter

io.Copy
ZipWriter
nil

golint
fsnotify https://fsnotify.org
https://github.com/fsnotify

os.FileInfo

type FileInfo interface {


Name() string // base name of the file
Size() int64 // length in bytes for regular files;
system-dependent for others
Mode() FileMode // file mode bits
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
Sys() interface{} // underlying data source (can return nil)
}

dirhash.go

package backup
import (
"crypto/md5"
"fmt"
"io"
"os"
"path/filepath"
)
func DirHash(path string) (string, error) {
hash := md5.New()
err := filepath.Walk(path, func(path string, info os.FileInfo, err error)
error {
if err != nil {
return err
}
io.WriteString(hash, path)
fmt.Fprintf(hash, "%v", info.IsDir())
fmt.Fprintf(hash, "%v", info.ModTime())
fmt.Fprintf(hash, "%v", info.Mode())
fmt.Fprintf(hash, "%v", info.Name())
fmt.Fprintf(hash, "%v", info.Size())
return nil
})
if err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

hash.Hash
filepath.Walk

io.WriteString io.Writer
fmt.Fprintf
%v

fmt.Sprintf Sum hash.Hash

nil %x

Monitor Monitor
Archiver
backup.ZIP

monitor.go

type Monitor struct {


Paths map[string]string
Archiver Archiver
Destination string
}
Now

func (m *Monitor) Now() (int, error) {


var counter int
for path, lastHash := range m.Paths {
newHash, err := DirHash(path)
if err != nil {
return counter, err
}
if newHash != lastHash {
err := m.act(path)
if err != nil {
return counter, err
}
m.Paths[path] = newHash // update the hash
counter++
}
}
return counter, nil
}

Now

act

Now

m.act undefined (type *Monitor has no field or method act)

act

func (m *Monitor) act(path string) error {


dirname := filepath.Base(path)
filename := fmt.Sprintf("%d.zip", time.Now().UnixNano())
return m.Archiver.Archive(path, filepath.Join(m.Destination, dirname,
filename))
}

Archiver
Archive
Archive act Now

act time.Now().UnixNano()
.zip

Archiver
.zip

Archiver
Ext()
act
Archiver

archiver.go Archiver

type Archiver interface {


DestFmt() string
Archive(src, dest string) error
}

zipper

func (z *zipper) DestFmt() string {


return "%d.zip"
}

act Archiver
act

func (m *Monitor) act(path string) error {


dirname := filepath.Base(path)
filename := fmt.Sprintf(m.Archiver.DestFmt(), time.Now().UnixNano())
return m.Archiver.Archive(path, filepath.Join(m.Destination, dirname,
filename))
}

cmds backup backup


backup/cmds/backup

backup main.go

func main() {
var fatalErr error
defer func() {
if fatalErr != nil {
flag.PrintDefaults()
log.Fatalln(fatalErr)
}
}()
var (
dbpath = flag.String("db", "./backupdata", "path to database
directory")
)
flag.Parse()
args := flag.Args()
if len(args) < 1 {
fatalErr = errors.New("invalid usage; must specify command")
return
}
}

fatalErr
nil
db filedb
github.com/matryer/filedb

mgo
filedb

filedb

vendor

main

db, err := filedb.Dial(*dbpath)


if err != nil {
fatalErr = err
return
}
defer db.Close()
col, err := db.C("paths")
if err != nil {
fatalErr = err
return
}

filedb.Dial filedb
mgo C
col
fatalErr

path
filedb
struct main

type path struct {


Path string
Hash string
}

flag.Args os.Args
main

switch strings.ToLower(args[0]) {
case "list":
case "add":
case "remove":
}

backup LIST

ForEach col

var path path


col.ForEach(func(i int, data []byte) bool {
err := json.Unmarshal(data, &path)
if err != nil {
fatalErr = err
return true
}
fmt.Printf("= %s\n", path)
return false
})

ForEach
path
fmt.Printf false filedb
true

String representations for your own types


%s
String()

func (p path) String() string {


return fmt.Sprintf("%s [%s]", p.Path, p.Hash)
}

path
InsertJSON add

if len(args[1:]) == 0 {
fatalErr = errors.New("must specify path to add")
return
}
for _, p := range args[1:] {
path := &path{Path: p, Hash: "Not yet archived"}
if err := col.InsertJSON(path); err != nil {
fatalErr = err
return
}
fmt.Printf("+ %s\n", path)
}

backup
add
+
Not yet archived

RemoveEach
remove

var path path


col.RemoveEach(func(i int, data []byte) (bool, bool) {
err := json.Unmarshal(data, &path)
if err != nil {
fatalErr = err
return false, true
}
for _, p := range args[1:] {
if path.Path == p {
fmt.Printf("- %s\n", path)
return true, false
}
}
return false, false
})

RemoveEach

backup
backupdata backup/cmds/backup filedb

main.go
test3 remove

filedb
backup

backup backupd
filedb
backup

backupd backup/cmds/backup

func main() {
var fatalErr error
defer func() {
if fatalErr != nil {
log.Fatalln(fatalErr)
}
}()
var (
interval = flag.Duration("interval", 10 * time.Second, "interval
between
checks")
archive = flag.String("archive", "archive", "path to archive
location")
dbpath = flag.String("db", "./db", "path to filedb database")
)
flag.Parse()
}
interval archive db interval

archive db
filedb backup
flag.Parse

Monitor
main

m := &backup.Monitor{
Destination: *archive,
Archiver: backup.ZIP,
Paths: make(map[string]string),
}

backup.Monitor archive Destination


backup.ZIP

main

db, err := filedb.Dial(*dbpath)


if err != nil {
fatalErr = err
return
}
defer db.Close()
col, err := db.C("paths")
if err != nil {
fatalErr = err
return
}

paths fatalErr
main

type path struct {


Path string
Hash string
}

LastChecked backupd
backup
path

Paths

Paths

main

var path path


col.ForEach(func(_ int, data []byte) bool {
if err := json.Unmarshal(data, &path); err != nil {
fatalErr = err
return true
}
m.Paths[path.Path] = path.Hash
return false // carry on
})
if fatalErr != nil {
return
}
if len(m.Paths) < 1 {
fatalErr = errors.New("no paths - use backup tool to add at least one")
return
}

ForEach
path
Paths

Paths

break

for {}

select

check(m, col)
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
for {
select {
case <-time.After(*interval):
check(m, col)
case <-signalChan:
// stop
fmt.Println()
log.Printf("Stopping...")
return
}
}

check
signal.Notify
for
timer
timer check
signalChan

time.After
flag.Duration
* time.Duration
flag.Duration
10s 1m

check Now
Monitor

main

func check(m *backup.Monitor, col *filedb.C) {


log.Println("Checking...")
counter, err := m.Now()
if err != nil {
log.Fatalln("failed to backup:", err)
}
if counter > 0 {
log.Printf(" Archived %d directories\n", counter)
// update hashes
var path path
col.SelectEach(func(_ int, data []byte) (bool, []byte, bool) {
if err := json.Unmarshal(data, &path); err != nil {
log.Println("failed to unmarshal data (skipping):", err)
return true, data, false
}
path.Hash, _ = m.Paths[path.Path]
newdata, err := json.Marshal(&path)
if err != nil {
log.Println("failed to marshal data (skipping):", err)
return true, data, false
}
return true, newdata, false
})
} else {
log.Println(" No changes")
}
}

check
Now Monitor

SelectEach

backupd

backup

backupd
test test2
backupd

db
backup
archive
5

backupd
Not yet archived

archive backup/cmds/backupd
test test2

backupd

test2
test one.txt backupd
archive/test2

one.txt
test
Archiver Restore
encoding/zip

os io
encoding/zip

app.yaml

https://cloud.google.com/appengine/downloads

go_appengine
GOPATH /Users/yourname/work/go_appengine

https://github.com/matryer/goblueprint
s

go_appengine $PATH
go
goapp go
goapp test goapp vet

https://console.cloud.google.com

answersapp
init
http.Handle http.HandleFunc
main

GOPATH answersapp/api
main.go

package api
import (
"io"
"net/http"
)
func init() {
http.HandleFunc("/", handleHello)
}
func handleHello(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello from App Engine")
}

ListenAndServe
init main
handleHello

app.yaml
answersapp/api

application: YOUR_APPLICATION_ID_HERE
version: 1
runtime: go
api_version: go1
handlers:
- url: /.*
script: _go_app
application
version

runtime
go
api_version go1
go2
handlers
_go_app

answersapp/api
:56443
8000 default
localhost:8080
Hello from App Engine
:8080 :8000
https://YOUR_APPLICATION_ID_HERE.appspot.com/

config

api
default
default main.go

package defaultmodule
func init() {}

default

default

web
api default web

https://github.com/matryer/goblueprints

Chapter 9
README
web

/answersapp/api
/answersapp/default
/answersapp/web

api
app.yaml module

application: YOUR_APPLICATION_ID_HERE
version: 1
runtime: go
module: api
api_version: go1
handlers:
- url: /.*
script: _go_app
app.yaml api/app.yaml
default/app.yaml default

application: YOUR_APPLICATION_ID_HERE
version: 1
runtime: go
module: default
api_version: go1
handlers:
- url: /.*
script: _go_app

dispatch.yaml

/api/ api
web default

answersapp
dispatch.yaml

application: YOUR_APPLICATION_ID_HERE
dispatch:
- url: "*/api/*"
module: api
- url: "*/*"
module: web

application
dispatch
TweetBody
– AvatarURL UserAvatarURL

AvatarURL UserAvatarURL
“ ”

datastore

Answer
Create datastore

answersapp questions.go struct

type Question struct {


Key *datastore.Key `json:"id" datastore:"-"`
CTime time.Time `json:"created"`
Question string `json:"question"`
User UserCard `json:"user"`
AnswersCount int `json:"answers_count"`
}

UserCard
User

datastore
import "google.golang.org/appengine/datastore"

datastore.Key

datastore.NewKey datastore.NewIncompleteKey

datastore.Get datastore.Put

Key
Question datastore:"-" json
Key
func (q Question) OK() error {
if len(q.Question) < 10 {
return errors.New("question is too short")
}
return nil
}

OK
nil

Question
questions.go

func (q *Question) Create(ctx context.Context) error {


log.Debugf(ctx, "Saving question: %s", q.Question)
if q.Key == nil {
q.Key = datastore.NewIncompleteKey(ctx, "Question", nil)
}
user, err := UserFromAEUser(ctx)
if err != nil {
return err
}
q.User = user.Card()
q.CTime = time.Now()
q.Key, err = datastore.Put(ctx, q.Key, q)
if err != nil {
return err
}
return nil
}

Create Question

(q Question) *

Question
log https://godoc.org/google.golang.org/appen
gine/log

nil

context.Context
nil

User CTime
time.Now

Question datastore.Put
context.Context

datastore.Put error

datastore.Key
Key Question

nil

func (q *Question) Update(ctx context.Context) error {


if q.Key == nil {
q.Key = datastore.NewIncompleteKey(ctx, "Question", nil)
}
var err error
q.Key, err = datastore.Put(ctx, q.Key, q)
if err != nil {
return err
}
return nil
}

CTime User
datastore.Get
datastore
questions.go

func GetQuestion(ctx context.Context, key *datastore.Key)


(*Question, error) {
var q Question
err := datastore.Get(ctx, key, &q)
if err != nil {
return nil, err
}
q.Key = key
return &q, nil
}

GetQuestion context.Context datastore.Key


datastore.Get

datastore.Get datastore.Put

users.go

type User struct {


Key *datastore.Key `json:"id" datastore:"-"`
UserID string `json:"-"`
DisplayName string `json:"display_name"`
AvatarURL string `json:"avatar_url"`
Score int `json:"score"`
}
Question Key User

https://godoc.org/google.golang.org/appengine/user
user.Current(context.Context)
user.User

User

goimports os/user

users.go

func UserFromAEUser(ctx context.Context) (*User, error) {


aeuser := user.Current(ctx)
if aeuser == nil {
return nil, errors.New("not logged in")
}
var appUser User
appUser.Key = datastore.NewKey(ctx, "User", aeuser.ID, 0, nil)
err := datastore.Get(ctx, appUser.Key, &appUser)
if err != nil && err != datastore.ErrNoSuchEntity {
return nil, err
}
if err == nil {
return &appUser, nil
}
appUser.UserID = aeuser.ID
appUser.DisplayName = aeuser.String()
appUser.AvatarURL = gravatarURL(aeuser.Email)
log.Infof(ctx, "saving new user: %s", aeuser.String())
appUser.Key, err = datastore.Put(ctx, appUser.Key, &appUser)
if err != nil {
return nil, err
}
return &appUser, nil
}
user.Current nil

appUser User
datastore.Key
datastore.NewKey
User
User

Get

datastore.Get User

datastore.ErrNoSuchEntity
datastore.Put User

https://medium.com/@matryer

users.go

func gravatarURL(email string) string {


m := md5.New()
io.WriteString(m, strings.ToLower(email))
return fmt.Sprintf("//www.gravatar.com/avatar/%x", m.Sum(nil))
}

User
UserCard

User Key datastore:"-"


users.go UserCard
User

type UserCard struct {


Key *datastore.Key `json:"id"`
DisplayName string `json:"display_name"`
AvatarURL string `json:"avatar_url"`
}
func (u User) Card() UserCard {
return UserCard{
Key: u.Key,
DisplayName: u.DisplayName,
AvatarURL: u.AvatarURL,
}
}

UserCard datastore Key


Card() UserCard

Account
answers.go

type Answer struct {


Key *datastore.Key `json:"id" datastore:"-"`
Answer string `json:"answer"`
CTime time.Time `json:"created"`
User UserCard `json:"user"`
Score int `json:"score"`
}
func (a Answer) OK() error {
if len(a.Answer) < 10 {
return errors.New("answer is too short")
}
return nil
}

Answer datastore.Key
CTime UserCard
Score

Question AnswerCount

AnswerCount
datastore.RunInTransaction
answers.go

func (a *Answer) Create(ctx context.Context, questionKey *datastore.Key)


error {
a.Key = datastore.NewIncompleteKey(ctx, "Answer", questionKey)
user, err := UserFromAEUser(ctx)
if err != nil {
return err
}
a.User = user.Card()
a.CTime = time.Now()
err = datastore.RunInTransaction(ctx, func(ctx context.Context) error {
q, err := GetQuestion(ctx, questionKey)
if err != nil {
return err
}
err = a.Put(ctx)
if err != nil {
return err
}
q.AnswersCount++
err = q.Update(ctx)
if err != nil {
return err
}
return nil
}, &datastore.TransactionOptions{XG: true})
if err != nil {
return err
}
return nil
}

Answer

UserFromAEUser
UserCard Answer CTime

datastore.RunInTransaction

datastore.TransactionOptions
XG true
Answer Question

TransactionOptions
RunInTransaction
GetQuestion

AnswerCount
AnswerCount

Answer.Create

GetAnswer GetQuestion

func GetAnswer(ctx context.Context, answerKey *datastore.Key)


(*Answer, error) {
var answer Answer
err := datastore.Get(ctx, answerKey, &answer)
if err != nil {
return nil, err
}
answer.Key = answerKey
return &answer, nil
}

Put answers.go

func (a *Answer) Put(ctx context.Context) error {


var err error
a.Key, err = datastore.Put(ctx, a.Key, a)
if err != nil {
return err
}
return nil
}

GetQuestion Question.Put
datastore.Query

Score

answers.go

func GetAnswers(ctx context.Context, questionKey *datastore.Key)


([]*Answer, error) {
var answers []*Answer
answerKeys, err := datastore.NewQuery("Answer").
Ancestor(questionKey).
Order("-Score").
Order("-CTime").
GetAll(ctx, &answers)
for i, answer := range answers {
answer.Key = answerKeys[i]
}
if err != nil {
return nil, err
}
return answers, nil
}
Answer datastore.NewQuery
Ancestor
Order
Score GetAll

answer.Key datastore.Key
GetAll

Authorized
true

datastore.NewQuery("Answer").
Filter("Authorized =", true)
Query Question Order

Limit

questions.go TopQuestions

func TopQuestions(ctx context.Context) ([]*Question, error) {


var questions []*Question
questionKeys, err := datastore.NewQuery("Question").
Order("-AnswersCount").
Order("-CTime").
Limit(25).
GetAll(ctx, &questions)
if err != nil {
return nil, err
}
for i := range questions {
questions[i].Key = questionKeys[i]
}
return questions, nil
}

Question
questions.go datastore Question

type Question struct {


Key *datastore.Key `json:"id" datastore:"-"`
CTime time.Time `json:"created" datastore:",noindex"`
Question string `json:"question" datastore:",noindex"`
User UserCard `json:"user"`
AnswersCount int `json:"answers_count"`
}

datastore:",noindex"

,noindex

json

noindex
Answer

type Answer struct {


Key *datastore.Key `json:"id" datastore:"-"`
Answer string `json:"answer" datastore:",noindex"`
CTime time.Time `json:"created"`
User UserCard `json:"user" datastore:",noindex"`
Score int `json:"score"`
}

Vote

type Vote struct {


Key *datastore.Key `json:"id" datastore:"-"`
MTime time.Time `json:"last_modified" datastore:",noindex"`
Question QuestionCard `json:"question" datastore:",noindex"`
Answer AnswerCard `json:"answer" datastore:",noindex"`
User UserCard `json:"user" datastore:",noindex"`
Score int `json:"score" datastore:",noindex"`
}

noindex AnswerCard
UserCard QuestionCard

noindex

Vote
votes.go

type Vote struct {


Key *datastore.Key `json:"id" datastore:"-"`
MTime time.Time `json:"last_modified" datastore:",noindex"`
Question QuestionCard `json:"question" datastore:",noindex"`
Answer AnswerCard `json:"answer" datastore:",noindex"`
User UserCard `json:"user" datastore:",noindex"`
Score int `json:"score" datastore:",noindex"`
}
Vote Question
Answer User Score 1
-1
MTimetime.Time

*Card Vote
Vote

UserCard

questions.go QuestionCard

type QuestionCard struct {


Key *datastore.Key `json:"id" datastore:",noindex"`
Question string `json:"question" datastore:",noindex"`
User UserCard `json:"user" datastore:",noindex"`
}
func (q Question) Card() QuestionCard {
return QuestionCard{
Key: q.Key,
Question: q.Question,
User: q.User,
}
}

QuestionCard Question UserCard


CTime AnswersCount

AnswerCard answers.go

type AnswerCard struct {


Key *datastore.Key `json:"id" datastore:",noindex"`
Answer string `json:"answer" datastore:",noindex"`
User UserCard `json:"user" datastore:",noindex"`
}

func (a Answer) Card() AnswerCard {


return AnswerCard{
Key: a.Key,
Answer: a.Answer,
User: a.User,
}
}

Answer User CTime


Score

Answer
Answer


Score

votes.go

func CastVote(ctx context.Context, answerKey *datastore.Key, score int)


(*Vote, error) {
question, err := GetQuestion(ctx, answerKey.Parent())
if err != nil {
return nil, err
}
user, err := UserFromAEUser(ctx)
if err != nil {
return nil, err
}
var vote Vote
err = datastore.RunInTransaction(ctx, func(ctx context.Context) error {
var err error
vote, err = castVoteInTransaction(ctx, answerKey, question, user,
score)
if err != nil {
return err
}
return nil
}, &datastore.TransactionOptions{XG: true})
if err != nil {
return nil, err
}
return &vote, nil
}

CastVote Context datastore.Key

castVoteInTransaction

CastVote datastore.Key Question

Parent
http://bit.ly/lineofsightincode

CastVote

votes.go

func castVoteInTransaction(ctx context.Context, answerKey *datastore.Key,


question *Question, user *User, score int) (Vote, error) {
var vote Vote
answer, err := GetAnswer(ctx, answerKey)
if err != nil {
return vote, err
}
voteKeyStr := fmt.Sprintf("%s:%s", answerKey.Encode(), user.Key.Encode())
voteKey := datastore.NewKey(ctx, "Vote", voteKeyStr, 0, nil)
var delta int // delta describes the change to answer score
err = datastore.Get(ctx, voteKey, &vote)
if err != nil && err != datastore.ErrNoSuchEntity {
return vote, err
}
if err == datastore.ErrNoSuchEntity {
vote = Vote{
Key: voteKey,
User: user.Card(),
Answer: answer.Card(),
Question: question.Card(),
Score: score,
}
} else {
// they have already voted - so we will be changing
// this vote
delta = vote.Score * -1
}
delta += score
answer.Score += delta
err = answer.Put(ctx)
if err != nil {
return vote, err
}
vote.Key = voteKey
vote.Score = score
vote.MTime = time.Now()
err = vote.Put(ctx)
if err != nil {
return vote, err
}
return vote, nil
}

Vote

Vote

Vote

datastore.Get
datastore.ErrNoSuchEntity Vote
delta

1 -1
-1 1 2
-1 err !=
datastore.ErrNoSuchEntity
delta

Vote
CastVote datastore.RunInTransaction

v interface{} OK

if obj, ok := v.(interface{ OK() error }); ok {


// v has OK() method
} else {
// v does not have OK() method
}

v ok true obj
ok
http.go

func decode(r *http.Request, v interface{}) error {


err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
return err
}
if valid, ok := v.(interface {
OK() error
}); ok {
err = valid.OK()
if err != nil {
return err
}
}
return nil
}

http.Request v
OK
OK

respond http.go

func respond(ctx context.Context, w http.ResponseWriter,


r *http.Request, v interface{}, code int) {
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(v)
if err != nil {
respondErr(ctx, w, r, err, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type",
"application/json; charset=utf-8")
w.WriteHeader(code)
_, err = buf.WriteTo(w)
if err != nil {
log.Errorf(ctx, "respond: %s", err)
}
}

context ResponseWriter Request


v

respondErr http.go

func respondErr(ctx context.Context, w http.ResponseWriter,


r *http.Request, err error, code int) {
errObj := struct {
Error string `json:"error"`
}{ Error: err.Error() }
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
err = json.NewEncoder(w).Encode(errObj)
if err != nil {
log.Errorf(ctx, "respondErr: %s", err)
}
}

error
error
func TestPathParams(t *testing.T) {
r, err := http.NewRequest("GET", "1/2/3/4/5", nil)
if err != nil {
t.Errorf("NewRequest: %s", err)
}
params := pathParams(r, "one/two/three/four")
if len(params) != 4 {
t.Errorf("expected 4 params but got %d: %v", len(params), params)
}
for k, v := range map[string]string{
"one": "1",
"two": "2",
"three": "3",
"four": "4",
} {
if params[k] != v {
t.Errorf("%s: %s != %s", k, params[k], v)
}
}
params = pathParams(r, "one/two/three/four/five/six")
if len(params) != 5 {
t.Errorf("expected 5 params but got %d: %v", len(params), params)
}
for k, v := range map[string]string{
"one": "1",
"two": "2",
"three": "3",
"four": "4",
"five": "5",
} {
if params[k] != v {
t.Errorf("%s: %s != %s", k, params[k], v)
}
}
}

http.Request

go test -v
http.go

func pathParams(r *http.Request,pattern string) map[string]string{


params := map[string]string{}
pathSegs := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
for i, seg := range strings.Split(strings.Trim(pattern, "/"), "/") {
if i > len(pathSegs)-1 {
return params
}
params[seg] = pathSegs[i]
}
return params
}

http.Request
/questions/id
/questions/123

questions id

POST /questions
GET /questions/{id}
GET /questions
switch
pathParams

handle_questions.go
http.HandlerFunc

func handleQuestions(w http.ResponseWriter, r *http.Request) {


switch r.Method {
case "POST":
handleQuestionCreate(w, r)
case "GET":
params := pathParams(r, "/api/questions/:id")
questionID, ok := params[":id"]
if ok { // GET /api/questions/ID
handleQuestionGet(w, r, questionID)
return
}
handleTopQuestions(w, r) // GET /api/questions/
default:
http.NotFound(w, r)
}
}

POST handleQuestionCreate GET


handleQuestionGet
handleTopQuestions

context.Context

Context
Context
ht
tps://blog.golang.org/context

appengine.NewContext
http.Request

func handleQuestionCreate(w http.ResponseWriter, r *http.Request) {


ctx := appengine.NewContext(r)
var q Question
err := decode(r, &q)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
err = q.Create(ctx)
if err != nil {
respondErr(ctx, w, r, err, http.StatusInternalServerError)
return
}
respond(ctx, w, r, q, http.StatusCreated)
}

Context ctx

OK Create

respondErr

Question http.StatusCreated

datastore.Key id
json

datastore.Key datastore
datastore.DecodeKey
handle_questions.go

func handleQuestionGet(w http.ResponseWriter, r *http.Request,


questionID string) {
ctx := appengine.NewContext(r)
questionKey, err := datastore.DecodeKey(questionID)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
question, err := GetQuestion(ctx, questionKey)
if err != nil {
if err == datastore.ErrNoSuchEntity {
respondErr(ctx, w, r, datastore.ErrNoSuchEntity,
http.StatusNotFound)
return
}
respondErr(ctx, w, r, err, http.StatusInternalServerError)
return
}
respond(ctx, w, r, question, http.StatusOK)
}

question ID
datastore.Key question ID

question ID
datastore.Key GetQuestion Question
datastore.ErrNoSuchEntity
http.StatusInternalServerError

respond
POST /answers
GET /answers
handle_answers.go http.HandlerFunc

func handleAnswers(w http.ResponseWriter, r *http.Request) {


switch r.Method {
case "GET":
handleAnswersGet(w, r)
case "POST":
handleAnswerCreate(w, r)
default:
http.NotFound(w, r)
}
}

GET handleAnswersGet POST


handleAnswerCreate 404 Not Found

func handleAnswersGet(w http.ResponseWriter, r *http.Request) {


ctx := appengine.NewContext(r)
q := r.URL.Query()
questionIDStr := q.Get("question_id")
questionKey, err := datastore.DecodeKey(questionIDStr)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
answers, err := GetAnswers(ctx, questionKey)
if err != nil {
respondErr(ctx, w, r, err, http.StatusInternalServerError)
return
}
respond(ctx, w, r, answers, http.StatusOK)
}
r.URL.Query() http.Values
question_id

/api/answersquestion_id=abc123

/api/answers

Answer
datastore.Key

handle_answers.go handleAnswerCreate

func handleAnswerCreate(w http.ResponseWriter, r *http.Request) {


ctx := appengine.NewContext(r)
var newAnswer struct {
Answer
QuestionID string `json:"question_id"`
}
err := decode(r, &newAnswer)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
questionKey, err := datastore.DecodeKey(newAnswer.QuestionID)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
err = newAnswer.OK()
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
answer := newAnswer.Answer
user, err := UserFromAEUser(ctx)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
answer.User = user.Card()
err = answer.Create(ctx, questionKey)
if err != nil {
respondErr(ctx, w, r, err, http.StatusInternalServerError)
return
}
respond(ctx, w, r, answer, http.StatusCreated)
}

var newAnswer struct


newAnswer
QuestionID string Answer
Answer QuestionID
datastore.Key
User UserCard Card

Create

/votes

handle_votes.go

func handleVotes(w http.ResponseWriter, r *http.Request) {


if r.Method != "POST" {
http.NotFound(w, r)
return
}
handleVote(w, r)
}
POST
handleVote

OK

-1 1

func validScore(score int) bool {


return score == -1 || score == 1
}

votes.go validScore

func validScore(score int) error {


if score != -1 && score != 1 {
return errors.New("invalid score")
}
return nil
}

nil

handleVote
handle_votes.go

func handleVote(w http.ResponseWriter, r *http.Request) {


ctx := appengine.NewContext(r)
var newVote struct {
AnswerID string `json:"answer_id"`
Score int `json:"score"`
}
err := decode(r, &newVote)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
err = validScore(newVote.Score)
if err != nil {
respondErr(ctx, w, r, err, http.StatusBadRequest)
return
}
answerKey, err := datastore.DecodeKey(newVote.AnswerID)
if err != nil {
respondErr(ctx, w, r, errors.New("invalid answer_id"),
http.StatusBadRequest)
return
}
vote, err := CastVote(ctx, answerKey, newVote.Score)
if err != nil {
respondErr(ctx, w, r, err, http.StatusInternalServerError)
return
}
respond(ctx, w, r, vote, http.StatusCreated)
}

handle_

main.go init

func init() {
http.HandleFunc("/api/questions/", handleQuestions)
http.HandleFunc("/api/answers/", handleAnswers)
http.HandleFunc("/api/votes/", handleVotes)
}

handleHello
goapp

:8080
dispatch.yaml

localhost:8080
localhost:8000
Automatically generated indexes

index.yaml

appcfg.py

https://YOUR_APPLICATION_ID_HERE.appspot.com/

The index for this query is not


ready to serve


https://gokit.io
@peterbourgon

”—

vault –
func veryLongFunctionWithLotsOfArguments(one string, two int, three
http.Handler, four string) (bool, error) {
log.Println("first line of the function")
}

POST
PUT DELETE

POST /users
{
"name": "Mat",
"twitter": "@matryer"
}
201 Created
{
"id": 1,
"name": "Mat",
"twitter": "@matryer"
}

protobuf

<person>
<name>MAT</name>
</person>

{"name":"MAT"}

.proto

https://github.com/google/proto
buf/releases protoc

$PATH

proto3
$GOPATH vault pb
pb

Vault Hash
Validate

Hash

Validate

pb
vault.proto

syntax = "proto3";
package pb;
service Vault {
rpc Hash(HashRequest) returns (HashResponse) {}
rpc Validate(ValidateRequest) returns (ValidateResponse) {}
}
message HashRequest {
string password = 1;
}
message HashResponse {
string hash = 1;
string err = 2;
}
message ValidateRequest {
string password = 1;
string hash = 2;
}
message ValidateResponse {
bool valid = 1;
}
proto3
pb

service Vault HashRequest


HashResponse ValidateRequest ValidateResponse
rpc
Hash Validate

type name = position;

type string bool double


float int32 int64 name
hash password

https://developers.go
ogle.com/protocol-buffers/docs/proto3

HashRequest

HashResponse ValidateResponse
error
pb

vault.pb.go

VaultClient VaultServer

pb

vault service.go

// Service provides password hashing capabilities.


type Service interface {
Hash(ctx context.Context, password string) (string,
error)
Validate(ctx context.Context, password, hash string)
(bool, error)
}

VaultService
Service
vault.Service

Hash Validate context.Context


string
string bool error
golang.org/x/net/context context

struct

service.go struct

type vaultService struct{}

service_test.go

package vault
import (
"testing"
"golang.org/x/net/context"
)
func TestHasherService(t *testing.T) {
srv := NewService()
ctx := context.Background()
h, err := srv.Hash(ctx, "password")
if err != nil {
t.Errorf("Hash: %s", err)
}
ok, err := srv.Validate(ctx, "password", h)
if err != nil {
t.Errorf("Valid: %s", err)
}
if !ok {
t.Error("expected true from Valid")
}
ok, err = srv.Validate(ctx, "wrong password", h)
if err != nil {
t.Errorf("Valid: %s", err)
}
if ok {
t.Error("expected false from Valid")
}
}

NewService Hash
Validate
Validate false –

vaultService Service

vaultService NewService

// NewService makes a new Service.


func NewService() Service {
return vaultService{}
}

vaultService

Hash

Validate

https://codahale.co
m/how-to-safely-store-a-password/

bcrypt

service.go Hash

func (vaultService) Hash(ctx context.Context, password


string) (string, error) {
hash, err :=
bcrypt.GenerateFromPassword([]byte(password),
bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
bcrypt
golang.org/x/crypto/bcrypt
GenerateFromPassword

Hash (vaultService)
struct

Validate

func (vaultService) Validate(ctx context.Context,


password, hash string) (bool, error) {
err := bcrypt.CompareHashAndPassword([]byte(hash),
[]byte(password))
if err != nil {
return false, nil
}
return true, nil
}

Hash bcrypt.CompareHashAndPassword

false true

struct

Hash
service.go

type hashRequest struct {


Password string `json:"password"`
}
type hashResponse struct {
Hash string `json:"hash"`
Err string `json:"err,omitempty"`
}
hashRequest hashResponse
Err

struct
struct

Validate Service

http.DecodeRequestFunc
http.Request service.go

func decodeHashRequest(ctx context.Context, r


*http.Request) (interface{}, error) {
var req hashRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return nil, err
}
return req, nil
}

decodeHashRequest
json.Decoder
hashRequest

Validate

type validateRequest struct {


Password string `json:"password"`
Hash string `json:"hash"`
}
type validateResponse struct {
Valid bool `json:"valid"`
Err string `json:"err,omitempty"`
}
func decodeValidateRequest(ctx context.Context,
r *http.Request) (interface{}, error) {
var req validateRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return nil, err
}
return req, nil
}

validateRequest Password Hash


bool
Valid Err

hashResponse validateResponse

service.go

func encodeResponse(ctx context.Context,


w http.ResponseWriter, response interface{})
error {
return json.NewEncoder(w).Encode(response)
}

encodeResponse json.Encoder
response interface{}
http.ResponseWriter

endpoint

type Endpoint func(ctx context.Context, request


interface{})
(response interface{}, err error)

context.Context request response


error request response interface{}

http.Handler http.HandlerFunc
Endpoint

Service

endpoint.Endpoint
hashRequest Hash
hashResponse

service.go MakeHashEndpoint

func MakeHashEndpoint(srv Service) endpoint.Endpoint {


return func(ctx context.Context, request interface{})
(interface{}, error) {
req := request.(hashRequest)
v, err := srv.Hash(ctx, req.Password)
if err != nil {
return hashResponse{v, err.Error()}, nil
}
return hashResponse{v, ""}, nil
}
}

Service
Service
hashRequest
Hash Password hashRequest
hashResponse Hash

Validate

func MakeValidateEndpoint(srv Service) endpoint.Endpoint {


return func(ctx context.Context, request interface{})
(interface{}, error) {
req := request.(validateRequest)
v, err := srv.Validate(ctx, req.Password, req.Hash)
if err != nil {
return validateResponse{false, err.Error()}, nil
}
return validateResponse{v, ""}, nil
}
}

Endpoint

Hash
hashResponse
Error

vault.Service

service.go

type Endpoints struct {


HashEndpoint endpoint.Endpoint
ValidateEndpoint endpoint.Endpoint
}

vault.Service
Endpoints
Hash

func (e Endpoints) Hash(ctx context.Context, password


string) (string, error) {
req := hashRequest{Password: password}
resp, err := e.HashEndpoint(ctx, req)
if err != nil {
return "", err
}
hashResp := resp.(hashResponse)
if hashResp.Err != "" {
return "", errors.New(hashResp.Err)
}
return hashResp.Hash, nil
}

HashEndpoint hashRequest
hashResponse

func (e Endpoints) Validate(ctx context.Context, password,


hash string) (bool, error) {
req := validateRequest{Password: password, Hash: hash}
resp, err := e.ValidateEndpoint(ctx, req)
if err != nil {
return false, err
}
validateResp := resp.(validateResponse)
if validateResp.Err != "" {
return false, errors.New(validateResp.Err)
}
return validateResp.Valid, nil
}
server_http.go

package vault
import (
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
"golang.org/x/net/context"
)
func NewHTTPServer(ctx context.Context, endpoints
Endpoints) http.Handler {
m := http.NewServeMux()
m.Handle("/hash", httptransport.NewServer(
ctx,
endpoints.HashEndpoint,
decodeHashRequest,
encodeResponse,
))
m.Handle("/validate", httptransport.NewServer(
ctx,
endpoints.ValidateEndpoint,
decodeValidateRequest,
encodeResponse,
))
return m
}

github.com/go-kit/kit/transport/http
net/http
httptransport

NewServeMux http.Handler
/hash /validate
Endpoints
httptransport.NewServer

context.Context
pb
pb.VaultServer

type VaultServer interface {


Hash(context.Context, *HashRequest)
(*HashResponse, error)
Validate(context.Context, *ValidateRequest)
(*ValidateResponse, error)
}

Service

server_grpc.go

package vault
import (
"golang.org/x/net/context"
grpctransport "github.com/go-kit/kit/transport/grpc"
)
type grpcServer struct {
hash grpctransport.Handler
validate grpctransport.Handler
}
func (s *grpcServer) Hash(ctx context.Context,
r *pb.HashRequest) (*pb.HashResponse, error) {
_, resp, err := s.hash.ServeGRPC(ctx, r)
if err != nil {
return nil, err
}
return resp.(*pb.HashResponse), nil
}
func (s *grpcServer) Validate(ctx context.Context,
r *pb.ValidateRequest) (*pb.ValidateResponse, error) {
_, resp, err := s.validate.ServeGRPC(ctx, r)
if err != nil {
return nil, err
}
return resp.(*pb.ValidateResponse), nil
}
github.com/go-kit/kit/transport/grpc
grpctransport pb

grpcServer
grpctransport.Handler
ServeGRPC

pb
service.go

https://github.com/matryer/gobluepri
nts

server_grpc.go

func EncodeGRPCHashRequest(ctx context.Context,


r interface{}) (interface{}, error) {
req := r.(hashRequest)
return &pb.HashRequest{Password: req.Password}, nil
}

EncodeRequestFunc
hashRequest
interface{}
hashRequest
pb.HashRequest

server_grpc.go

func DecodeGRPCHashRequest(ctx context.Context,


r interface{}) (interface{}, error) {
req := r.(*pb.HashRequest)
return hashRequest{Password: req.Password}, nil
}
func EncodeGRPCHashResponse(ctx context.Context,
r interface{}) (interface{}, error) {
res := r.(hashResponse)
return &pb.HashResponse{Hash: res.Hash, Err: res.Err},
nil
}
func DecodeGRPCHashResponse(ctx context.Context,
r interface{}) (interface{}, error) {
res := r.(*pb.HashResponse)
return hashResponse{Hash: res.Hash, Err: res.Err}, nil
}
func EncodeGRPCValidateRequest(ctx context.Context,
r interface{}) (interface{}, error) {
req := r.(validateRequest)
return &pb.ValidateRequest{Password: req.Password,
Hash: req.Hash}, nil
}
func DecodeGRPCValidateRequest(ctx context.Context,
r interface{}) (interface{}, error) {
req := r.(*pb.ValidateRequest)
return validateRequest{Password: req.Password,
Hash: req.Hash}, nil
}
func EncodeGRPCValidateResponse(ctx context.Context,
r interface{}) (interface{}, error) {
res := r.(validateResponse)
return &pb.ValidateResponse{Valid: res.Valid}, nil
}
func DecodeGRPCValidateResponse(ctx context.Context,
r interface{}) (interface{}, error) {
res := r.(*pb.ValidateResponse)
return validateResponse{Valid: res.Valid}, nil
}
grpcServer grpcServer

func NewGRPCServer(ctx context.Context, endpoints


Endpoints) pb.VaultServer {
return &grpcServer{
hash: grpctransport.NewServer(
ctx,
endpoints.HashEndpoint,
DecodeGRPCHashRequest,
EncodeGRPCHashResponse,
),
validate: grpctransport.NewServer(
ctx,
endpoints.ValidateEndpoint,
DecodeGRPCValidateRequest,
EncodeGRPCValidateResponse,
),
}
}

Endpoints

grpcServer hash validate


grpctransport.NewServer endpoint.Endpoint

vault

vault cmd vaultd


vaultd
main vaultd
cmd cmd

main
htt
ps://github.com/matryer/drop main

go install

main main.go vaultd

import (
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"your/path/to/vault"
"your/path/to/vault/pb"
"golang.org/x/net/context"
"google.golang.org/grpc"
)

your/path/to $GOPATH

grpc
main
main

func main() {
var (
httpAddr = flag.String("http", ":8080",
"http listen address")
gRPCAddr = flag.String("grpc", ":8081",
"gRPC listen address")
)
flag.Parse()
ctx := context.Background()
srv := vault.NewService()
errChan := make(chan error)

:8080
8081

context.Background()

NewService Service

errChan

go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errChan <- fmt.Errorf("%s", <-c)
}()

signal.Notify SIGINT
SIGTERM c
String()
errChan
hashEndpoint := vault.MakeHashEndpoint(srv)
validateEndpoint := vault.MakeValidateEndpoint(srv)
endpoints := vault.Endpoints{
HashEndpoint: hashEndpoint,
ValidateEndpoint: validateEndpoint,
}

endpoints
srv

// HTTP transport
go func() {
log.Println("http:", *httpAddr)
handler := vault.NewHTTPServer(ctx, endpoints)
errChan <- http.ListenAndServe(*httpAddr, handler)
}()

NewHTTPServer

http.ListenAndServe
httpAddr
go func() {
listener, err := net.Listen("tcp", *gRPCAddr)
if err != nil {
errChan <- err
return
}
log.Println("grpc:", *gRPCAddr)
handler := vault.NewGRPCServer(ctx, endpoints)
gRPCServer := grpc.NewServer()
pb.RegisterVaultServer(gRPCServer, handler)
errChan <- gRPCServer.Serve(listener)
}()

gRPCAddr
errChan vault.NewGRPCServer
Endpoints

grpc
pb RegisterVaultServer

RegisterVaultService RegisterService
grpcServer
vault.pb.go
RegisterVaultServer
&_Vault_serviceDesc

Serve
errChan

<-errChan

log.Fatalln(<-errChan)
}

curl

vault/cmd/vaultd
curl

hernandez
vault/client/grpc
grpc
vault.Service

vault/client/grpc

client.go

func New(conn *grpc.ClientConn) vault.Service {


var hashEndpoint = grpctransport.NewClient(
conn, "Vault", "Hash",
vault.EncodeGRPCHashRequest,
vault.DecodeGRPCHashResponse,
pb.HashResponse{},
).Endpoint()
var validateEndpoint = grpctransport.NewClient(
conn, "Vault", "Validate",
vault.EncodeGRPCValidateRequest,
vault.DecodeGRPCValidateResponse,
pb.ValidateResponse{},
).Endpoint()
return vault.Endpoints{
HashEndpoint: hashEndpoint,
ValidateEndpoint: validateEndpoint,
}
}

grpctransport github.com/go-kit/kit/transport/grpc

Vault
Hash Validate
vault.Endpoints
vault.Service
cmd vaultcli

func main() {
var (
grpcAddr = flag.String("addr", ":8081",
"gRPC address")
)
flag.Parse()
ctx := context.Background()
conn, err := grpc.Dial(*grpcAddr, grpc.WithInsecure(),
grpc.WithTimeout(1*time.Second))
if err != nil {
log.Fatalln("gRPC dial:", err)
}
defer conn.Close()
vaultService := grpcclient.New(conn)
args := flag.Args()
var cmd string
cmd, args = pop(args)
switch cmd {
case "hash":
var password string
password, args = pop(args)
hash(ctx, vaultService, password)
case "validate":
var password, hash string
password, args = pop(args)
hash, args = pop(args)
validate(ctx, vaultService, password, hash)
default:
log.Fatalln("unknown command", cmd)
}
}

vault/client/grpc grpcclient
google.golang.org/grpc grpc vault

vault.Service

os.Args
flags.Args()

pop

pop

vaultcli main_test.go

func TestPop(t *testing.T) {


args := []string{"one", "two", "three"}
var s string
s, args = pop(args)
if s != "one" {
t.Errorf("unexpected "%s"", s)
}
s, args = pop(args)
if s != "two" {
t.Errorf("unexpected "%s"", s)
}
s, args = pop(args)
if s != "three" {
t.Errorf("unexpected "%s"", s)
}
s, args = pop(args)
if s != "" {
t.Errorf("unexpected "%s"", s)
}
}

main.go pop

func pop(s []string) (string, []string) {


if len(s) == 0 {
return "", s
}
return s[0], s[1:]
}

func hash(ctx context.Context, service vault.Service,


password string) {
h, err := service.Hash(ctx, password)
if err != nil {
log.Fatalln(err.Error())
}
fmt.Println(h)
}
func validate(ctx context.Context, service vault.Service,
password, hash string) {
valid, err := service.Validate(ctx, password, hash)
if err != nil {
log.Fatalln(err.Error())
}
if !valid {
fmt.Println("invalid")
os.Exit(1)
}
fmt.Println("valid")
}

vaultcli

$GOPATH/bin
$PATH

cmd
cmd/vaultd

$'PASTE_HASH_HERE'

!PASTE_HASH_HERE!

valid
invalid
h
ttps://en.wikipedia.org/wiki/Token_bucket
github.com/juju/ratelimit

github.com/juju/ratelimit hashEndpoint

rlbucket := ratelimit.NewBucket(1*time.Second, 5)

NewBucket

ratelimit

import ratelimitkit "github.com/go-kit/kit/ratelimit"

endpoint.Middleware

type Middleware func(Endpoint) Endpoint

Endpoint Endpoint
Endpoint

type Endpoint func(ctx context.Context, request


interface{}) (response interface{}, err error)

http.HandlerFunc Endpoint
Endpoint
Middleware
NewTokenBucketLimiter ratelimit

TakeAvailable
next

func NewTokenBucketLimiter(tb *ratelimit.Bucket)


endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{})
(interface{}, error) {
if tb.TakeAvailable(1) == 0 {
return nil, ErrLimited
}
return next(ctx, request)
}
}
}

e := getEndpoint(srv)
{
e = getSomeMiddleware()(e)
e = getLoggingMiddleware(logger)(e)
e = getAnotherMiddleware(something)(e)
}

hashEndpoint := vault.MakeHashEndpoint(srv)
{
hashEndpoint = ratelimitkit.NewTokenBucketLimiter
(rlbucket)(hashEndpoint)
}
validateEndpoint := vault.MakeValidateEndpoint(srv)
{
validateEndpoint = ratelimitkit.NewTokenBucketLimiter
(rlbucket)(validateEndpoint)
}
endpoints := vault.Endpoints{
HashEndpoint: hashEndpoint,
ValidateEndpoint: validateEndpoint,
}

hashEndpoint
validateEndpoint vault.Endpoints

errChan


NewTokenBucketThrottler

hashEndpoint := vault.MakeHashEndpoint(srv)
{
hashEndpoint = ratelimitkit.NewTokenBucketThrottler(rlbucket,
time.Sleep)(hashEndpoint)
}
validateEndpoint := vault.MakeValidateEndpoint(srv)
{
validateEndpoint = ratelimitkit.NewTokenBucketThrottler(rlbucket,
time.Sleep)(validateEndpoint)
}
endpoints := vault.Endpoints{
HashEndpoint: hashEndpoint,
ValidateEndpoint: validateEndpoint,
}

NewTokenBucketThrottler
time.Sleep

time.Sleep
proto3

bcrypt

https://gokit.io #go-kit


Chapter 9

https://github.com/docker/docker

docker
Chapter 10

https://www.docker.com/products/docker

https://github.com/matryer/gob
lueprints

Dockerfile
vault Chapter 10
Dockerfile

FROM scratch
MAINTAINER Your Name <your@email.address>
ADD vaultd vaultd
EXPOSE 8080 8081
ENTRYPOINT ["/vaultd"]

Dockerfile

FROM

https://hub.docker.com/_/scratch/
ADD vaultd
vaultd
EXPOSE :8080
:8081
ENTRYPOINT vaultd

MAINTAINER

https://docs.docker.com/engine/reference/builde
r/#dockerfile-reference
CGO_ENABLED GOOS
-a ./cmd/vaultd/
vaultd

CGO_ENABLED=0

GOOS

https://github.com/golang/go/
blob/master/src/go/build/syslist.go

vaultd

Dockerfile

docker
-t
vaultd

scratch
ADD

docker run vaultd

-p

8080 6060 8081 6061

localtest --name
--rm
curl

docker ps

0b5e35dca7cc
vaultd
/bin/sh -c /go/bin/vaultd
3 seconds ago
Up 2 seconds
0.0.0.0:6060->8080/tcp, 0.0.0.0:6061->8081/tcp
localtest
docker stop

localtest

https://hub.docker.com

login

WARNING: Error loading config,


permission denied sudo
USERNAME PASSWORD

vault USERNAME/vault

vault
vaultd

push

https://hub.docker.com/r/matryer/vault/
USERNAME/vault

curl

https://www.digitalocean.com
vault-service-1
Access console
root

docker

USERNAME

docker pull
matryer/vault
docker run -d

--rm

curl

IPADDRESS

curl
/validate
GOPATH

.go
https://g
olang.org/dl/

go/bin PATH

PATH=$PATH:/opt/go/bin
.bashrc

PATH
go/bin

PATH

go/bin

PATH
go go/bin

GOPATH PATH

import
GOPATH go get
GOPATH

GOPATH PATH
GOPATH
GOPATH

go Users Work
GOPATH

PATH
GOPATH go

silk
$GOPATH/src/github.com/matryer/silk

silk
matryer

GOPATH
github.com/matryer/goblueprints
GOPATH
GOPATH tooling main.go

package main
import (
"fmt"
)
func main() {
return
var name string
name = "Mat"
fmt.Println("Hello ", name)
}

package main
import (
"fmt"
)
func main() {
return
var name string
name = "Mat"
fmt.Println("Hello ", name)
}

go fmt

return go
vet

go vet

https://golang
.org/cmd/vet/

goimports
import

goimports import
goimports

fmt

import (
"net/http"
"sync"
)
go run main.go

fmt
goimports

goimports -w
.go

main.go net/http sync


fmt

fmt vet test


goimports
.go

goimports fmt
import

https://github.com/golang/go/wiki/ID
EsAndTextEditorPlugins
http://www.sublimetext.co
m/

https://github.com/DisposaBoy

GoSublime

GoSublime
https://sublime.wbond.net/

Package Control: Install


Package

"on_save": [
{
"cmd": "gs9o_open",
"args": {
"run": ["sh", "go build . errors && go test -i && go test &&
go vet && golint"],
"focus_view": false
}
}
]
on_save


tooling
main.go

main.go

on_save

on_save

goimports go fmt
main.go net/http fmt
fmt
https://code.visualstudio.com

.go
https://github.com/matryer/goblueprints/blob/master/appendixA/messycode/main.go
)
GOPATH
82

74
171 74 76
28 76 78
78 79
290
291 66
290 66
67
292 68
71 72
231 69 70
233
232
233
232
220
227
51 52
226
228
136
220
137
221 223
138 139
224 226
118 119 121
57

87
304
89
304

98 99
112
156
87
90
90
86
83 84 10
84 86
81 16 17
19 22 238
23 255

22 277
15 16 19 282
20 281
23 283 285
277 278
348 279 280 281
348
163
304 163 164 165

90 253 254 255


91 257 258
94 95 259
95 96 197 198 199
92 93 131
96 97
97 98 167
97
341
319 341
341
335
335 335
335
335 339 340
303 339

109 336
117 118 337

113 348
113 116 117 339
109 111 341
339 340
166 213 346 347
348

339
235
237 338
237
239 334
60 62 63
333 56 58
58 59
335 336
334
333
334 219

334
335
300 301
107
108
295
108
294
344 345 346
123 126
341 342 343 344
200 201
167
9
351
351
357 351
359 354
307 353 354

180 244
173 246 247
308 246
174 245
174 250 251
47 48 49
309 249 250
255 247 248
249
205 208
259 260 261
131 261 262
131 132
134 252
257 258
309 259
256
52 53 55 203
59
209 258
352

73 74
73
296 194 195
195 196
324
321 305
323 305
325 294
326
252
312 251
313 134

133
133
173

166 243
165 133
42
128 131
132
23 25 132
311

66
356
356 50
29 30 50
50

174
286 180
285 178
287 179
288 176 178
256

357
28

304 315 316


304 318
318
102 320
341 320

298 299 300 309


300
308
297 298
298 103
122 123
103
102
267 268
102
102
103

193 10
15
214 15
210 12
213 13
203 204 294
201
203 45
209 210
212 219
219
327 240
331
328 329 158 159
330
102 103
103 105 106
305
167 357 358
278 357
112
305
162 129 130
130
25 26 27 301
28 74 302
302
355 128
355
355 14
12 13
328 14
107
28 359 360 361
359 361
39
34 35 269
29 30 270 271 273
36
28 29 273
38 39 274 275 277
35 274
30 31
28 151
155 156
262 263 152
267 153 154
263 264 266 157
320
135 135
136 137
140
146
34 148 149
148 150
30 31 142
32 33 144 145 146
150 151
42

229
231 216
230
234 182
185
186 188
103

You might also like