├── .gitignore ├── section06 ├── .DS_Store ├── examples │ ├── app.yaml │ └── app.go └── README.md ├── section07 ├── local.png ├── examples │ └── app.go └── README.md ├── section09 ├── local.png ├── console.png ├── codec │ ├── app.yaml │ └── app.go ├── getset │ ├── app.yaml │ └── app.go └── README.md ├── events ├── screenshot.png ├── step3 │ ├── bad_api_output.json │ ├── app.yaml │ ├── good_api_output.json │ ├── README.md │ ├── static │ │ ├── script.js │ │ ├── style.css │ │ └── index.html │ ├── weather.go │ └── events.go ├── step2 │ ├── datastore.png │ ├── app.yaml │ ├── README.md │ ├── static │ │ ├── script.js │ │ ├── style.css │ │ └── index.html │ └── events.go ├── step0 │ ├── app.yaml │ ├── static │ │ ├── output.json │ │ ├── script.js │ │ ├── style.css │ │ └── index.html │ ├── README.md │ └── events.go ├── step1 │ ├── app.yaml │ ├── README.md │ ├── static │ │ ├── script.js │ │ ├── style.css │ │ └── index.html │ └── events.go ├── step5 │ ├── README.md │ ├── app.yaml │ ├── static │ │ ├── script.js │ │ ├── style.css │ │ └── index.html │ ├── weather.go │ └── events.go ├── step4 │ ├── app.yaml │ ├── README.md │ ├── static │ │ ├── script.js │ │ ├── style.css │ │ └── index.html │ ├── weather.go │ └── events.go └── README.md ├── section04 ├── img │ ├── plus.png │ ├── equals.png │ ├── gopher.png │ ├── gaegopher.jpg │ └── app-engine-logo.png ├── app │ ├── app.yaml │ └── app.go ├── examples │ └── hello.go └── README.md ├── section05 ├── screenshot.png ├── all_static │ ├── app.yaml │ ├── dummy.go │ └── hello.html ├── mixed_content │ ├── app.yaml │ ├── hello.go │ └── hello.html ├── static_dirs │ ├── app.yaml │ ├── static │ │ ├── style.css │ │ ├── script.js │ │ └── index.html │ └── hello.go └── README.md ├── utils └── http-methods │ ├── app.yaml │ └── app.go ├── .travis.yml ├── section08 ├── fetch │ ├── app.yaml │ └── fetch.go └── README.md ├── section00 ├── hello │ └── main.go └── README.md ├── section02 ├── examples │ ├── step1 │ │ └── main.go │ ├── step2 │ │ └── main.go │ ├── step3 │ │ └── main.go │ └── gorilla.go └── README.md ├── section01 ├── examples │ ├── get.go │ └── do-get.go └── README.md ├── section03 ├── examples │ ├── texthandler │ │ └── main.go │ └── handlers │ │ └── main.go └── README.md ├── CONTRIBUTING.md ├── section10 └── README.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | */.DS_Store -------------------------------------------------------------------------------- /section06/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section06/.DS_Store -------------------------------------------------------------------------------- /section07/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section07/local.png -------------------------------------------------------------------------------- /section09/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section09/local.png -------------------------------------------------------------------------------- /events/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/events/screenshot.png -------------------------------------------------------------------------------- /section04/img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section04/img/plus.png -------------------------------------------------------------------------------- /section09/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section09/console.png -------------------------------------------------------------------------------- /events/step3/bad_api_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "cod": "404", 3 | "message": "Error: Not found city" 4 | } -------------------------------------------------------------------------------- /section04/img/equals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section04/img/equals.png -------------------------------------------------------------------------------- /section04/img/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section04/img/gopher.png -------------------------------------------------------------------------------- /section05/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section05/screenshot.png -------------------------------------------------------------------------------- /events/step2/datastore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/events/step2/datastore.png -------------------------------------------------------------------------------- /section04/img/gaegopher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section04/img/gaegopher.jpg -------------------------------------------------------------------------------- /section06/examples/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /.* 6 | script: _go_app 7 | -------------------------------------------------------------------------------- /utils/http-methods/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /.* 6 | script: _go_app 7 | -------------------------------------------------------------------------------- /section04/img/app-engine-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/campoy/go-web-workshop/HEAD/section04/img/app-engine-logo.png -------------------------------------------------------------------------------- /section05/all_static/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /hello 6 | static_files: hello.html 7 | upload: hello.html 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | 6 | install: 7 | - go get github.com/campoy/embedmd 8 | script: 9 | - embedmd -d **/*.md 10 | - go get ./...&& go test ./... 11 | -------------------------------------------------------------------------------- /events/step0/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /api/.* 6 | script: _go_app 7 | - url: / 8 | static_files: static/index.html 9 | upload: static/index.html 10 | - url: / 11 | static_dir: static 12 | -------------------------------------------------------------------------------- /events/step1/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /api/.* 6 | script: _go_app 7 | - url: / 8 | static_files: static/index.html 9 | upload: static/index.html 10 | - url: / 11 | static_dir: static 12 | -------------------------------------------------------------------------------- /events/step2/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /api/.* 6 | script: _go_app 7 | - url: / 8 | static_files: static/index.html 9 | upload: static/index.html 10 | - url: / 11 | static_dir: static 12 | -------------------------------------------------------------------------------- /events/step5/README.md: -------------------------------------------------------------------------------- 1 | # Step 5: final version of the application 2 | 3 | Congratulations, your application is now complete!!!! 🎉 4 | 5 | You might continue improving it; one idea is to perform all 6 | the calls to `weather` concurrently. How would you do that? 7 | -------------------------------------------------------------------------------- /section04/app/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go # the runtime (python, java, go, php) 2 | api_version: go1 # the runtime version 3 | 4 | handlers: 5 | - url: /.* # for all requests 6 | script: _go_app # pass the request to the Go code -------------------------------------------------------------------------------- /section08/fetch/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go # the runtime (python, java, go, php) 2 | api_version: go1 # the runtime version 3 | 4 | handlers: 5 | - url: /.* # for all requests 6 | script: _go_app # pass the request to the Go code 7 | 8 | -------------------------------------------------------------------------------- /section09/codec/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go # the runtime (python, java, go, php) 2 | api_version: go1 # the runtime version 3 | 4 | handlers: 5 | - url: /.* # for all requests 6 | script: _go_app # pass the request to the Go code 7 | 8 | -------------------------------------------------------------------------------- /section09/getset/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go # the runtime (python, java, go, php) 2 | api_version: go1 # the runtime version 3 | 4 | handlers: 5 | - url: /.* # for all requests 6 | script: _go_app # pass the request to the Go code 7 | 8 | -------------------------------------------------------------------------------- /section05/mixed_content/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | # requests with empty paths are shown the html page. 6 | - url: / 7 | static_files: hello.html 8 | upload: hello.html 9 | # requests with the /api/ path are handled by the Go app. 10 | - url: /api/.* 11 | script: _go_app 12 | -------------------------------------------------------------------------------- /events/step4/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /api/.* 6 | script: _go_app 7 | - url: / 8 | static_files: static/index.html 9 | upload: static/index.html 10 | - url: / 11 | static_dir: static 12 | 13 | # Sign up to openweathermap.org and obtain a new API key, then replace the value of WEATHER_API_KEY. 14 | env_variables: 15 | WEATHER_API_KEY: 'get your own!' -------------------------------------------------------------------------------- /events/step3/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /api/.* 6 | script: _go_app 7 | - url: / 8 | static_files: static/index.html 9 | upload: static/index.html 10 | - url: / 11 | static_dir: static 12 | 13 | # Sign up to openweathermap.org and obtain a new API key, then replace the value of WEATHER_API_KEY. 14 | env_variables: 15 | WEATHER_API_KEY: 'get your own!' 16 | -------------------------------------------------------------------------------- /events/step5/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | - url: /api/.* 6 | script: _go_app 7 | - url: / 8 | static_files: static/index.html 9 | upload: static/index.html 10 | - url: / 11 | static_dir: static 12 | 13 | # Sign up to openweathermap.org and obtain a new API key, then replace the value of WEATHER_API_KEY. 14 | env_variables: 15 | WEATHER_API_KEY: 'get your own!' 16 | -------------------------------------------------------------------------------- /section05/static_dirs/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: go 2 | api_version: go1 3 | 4 | handlers: 5 | # requests starting with /api/ are handled by the Go app. 6 | - url: /api/.* 7 | script: _go_app 8 | 9 | # if the path is empty show index.html. 10 | - url: / 11 | static_files: static/index.html 12 | upload: static/index.html 13 | 14 | # otherwise try to find it in the static directory. 15 | - url: / 16 | static_dir: static 17 | -------------------------------------------------------------------------------- /events/step1/README.md: -------------------------------------------------------------------------------- 1 | # Step 1: JSON and local storage 2 | 3 | The goal of this step is to start encoding and encoding events. 4 | For now they will be stored in a global variable protected by a mutex (to avoid data races). 5 | This is not optimal because at any point the instance could disappear and we would lose our data. 6 | 7 | Don't worry too much about that, the next step takes care of storage with datastore. 8 | 9 | Once done, go back to the [JSON section](../../section06/README.md#congratulations). 10 | -------------------------------------------------------------------------------- /section05/all_static/dummy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package dummy 15 | -------------------------------------------------------------------------------- /section05/static_dirs/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | h1 { 17 | text-align: center; 18 | color: #a74; 19 | } 20 | -------------------------------------------------------------------------------- /section05/static_dirs/static/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | $(function() { 17 | $('#message').load('/api/hello'); 18 | }); 19 | -------------------------------------------------------------------------------- /section00/hello/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import "fmt" 17 | 18 | func main() { 19 | fmt.Println("hello, world!") } 20 | -------------------------------------------------------------------------------- /events/step4/README.md: -------------------------------------------------------------------------------- 1 | # Step 4: storing temporary results in Memcache 2 | 3 | In the previous step we ended up with an application that was wasting time 4 | and network resources by sending a new request to the weather API for each event, 5 | every time someone listed the events. 6 | 7 | Now we will fix that by using App Engine Memcache. This is simply a fully 8 | managed Memcache instance, you don't need to do anything to start using it! 9 | Isn't that cool? 10 | 11 | Once you're done with this exercise you will see that your application is much faster, 12 | and it consumes less resources. 13 | 14 | Congratulations, you're done! You can check your code comparing it to the one in 15 | [step 5](../step5), or go back to the [instructions](../../section09/README.md#congratulations) 16 | for the last instructions. 17 | -------------------------------------------------------------------------------- /section05/all_static/hello.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | Hello, App Engine 21 | 22 | 23 |

Hello, App Engine

24 | 25 | 26 | -------------------------------------------------------------------------------- /events/step0/static/output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Craft Conf", 4 | "description": "CRAFT is about software craftsmanship, which tools, methods, practices should be part of the toolbox of a modern developer and company.", 5 | "date": "2016-04-26T00:00:00Z", 6 | "location": "Budapest" 7 | }, 8 | { 9 | "title": "Google I/O", 10 | "description": "Google I/O is for developers - the creative coders who are building what's next. Each year, we explore the latest in tech, mobile \u0026 beyond.", 11 | "date": "2016-04-28T00:00:00Z", 12 | "location": "Mountain View" 13 | }, 14 | { 15 | "title": "GopherCon China", 16 | "description": "GOPHER'S BIGGEST PARTY", 17 | "date": "2016-05-18T00:00:00Z", 18 | "location": "Beijing" 19 | } 20 | ] -------------------------------------------------------------------------------- /events/step2/README.md: -------------------------------------------------------------------------------- 1 | # Step 2: durable storage 2 | 3 | This step adds durable storage by using [Google Cloud Datastore][1]. 4 | You can learn more about it by reading the docs. 5 | 6 | Everything we're going to use is the [Put][2] operation and [Queries][3]. 7 | When you see your application working again you should have a look at the 8 | Datastore Console either [the local one][4]. or the one on [Google Cloud Console][5]. 9 | 10 | 11 | 12 | Once you're done, go back to the instructions on [datastore](../../section07/README.md#congratulations). 13 | 14 | [1]: https://cloud.google.com/appengine/docs/go/datastore/ 15 | [2]: https://cloud.google.com/appengine/docs/go/datastore/reference#Put 16 | [3]: https://cloud.google.com/appengine/docs/go/datastore/queries 17 | [4]: http://localhost:8000/datastore?kind=Event 18 | [5]: https://console.cloud.google.com/datastore 19 | -------------------------------------------------------------------------------- /events/README.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | The goal of this workshop is to create and deploy a web application from scratch, 4 | using Go and Google App Engine. 5 | 6 | The application is an event manager with weather information for each event. Cool, right? 7 | It doesn't look amazing, but hey ... I'm *really* not a frontend engineer. 😅 8 | 9 | 10 | 11 | The implementation will be done in six steps starting, obviously, with step 0. 12 | 13 | - [Step 0: basic architecture](step0/README.md) 14 | - [Step 1: JSON and local storage](step1/README.md) 15 | - [Step 2: durable storage](step2/README.md) 16 | - [Step 3: adding weather with openweathermap.org](step3/README.md) 17 | - [Step 4: storing temporary results in Memcache](step4/README.md) 18 | - [Step 5: final version of the application](step5/README.md) 19 | 20 | Everything you need to do corresponds to a comment in the code, so don't miss any! 21 | -------------------------------------------------------------------------------- /section02/examples/step1/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | ) 20 | 21 | func helloHandler(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprintln(w, "Hello, web") 23 | } 24 | 25 | // This doesn't appear on the markdown docs. 26 | func main() {} 27 | -------------------------------------------------------------------------------- /section02/examples/step2/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | ) 20 | 21 | func helloHandler(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprintln(w, "Hello, web") 23 | } 24 | 25 | func main() { 26 | http.HandleFunc("/hello", helloHandler) 27 | } 28 | -------------------------------------------------------------------------------- /events/step3/good_api_output.json: -------------------------------------------------------------------------------- 1 | { 2 | "coord": { 3 | "lon": 19.04, 4 | "lat": 47.5 5 | }, 6 | "weather": [ 7 | { 8 | "id": 800, 9 | "main": "Clear", 10 | "description": "clear sky", 11 | "icon": "01n" 12 | } 13 | ], 14 | "base": "cmc stations", 15 | "main": { 16 | "temp": 277, 17 | "pressure": 1007, 18 | "humidity": 93, 19 | "temp_min": 275.37, 20 | "temp_max": 278.15 21 | }, 22 | "wind": { 23 | "speed": 1.5 24 | }, 25 | "clouds": { 26 | "all": 0 27 | }, 28 | "dt": 1461611729, 29 | "sys": { 30 | "type": 1, 31 | "id": 5724, 32 | "message": 0.0103, 33 | "country": "HU", 34 | "sunrise": 1461555317, 35 | "sunset": 1461606530 36 | }, 37 | "id": 3054643, 38 | "name": "Budapest", 39 | "cod": 200 40 | } -------------------------------------------------------------------------------- /section04/examples/hello.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package hello 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | ) 20 | 21 | func helloHandler(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprintln(w, "Hello, App Engine") 23 | } 24 | 25 | func init() { 26 | http.HandleFunc("/hello", helloHandler) 27 | } 28 | -------------------------------------------------------------------------------- /section05/mixed_content/hello.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package hello 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | ) 20 | 21 | func helloHandler(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprintln(w, "Hello, Backend") 23 | } 24 | 25 | func init() { 26 | http.HandleFunc("/api/hello", helloHandler) 27 | } 28 | -------------------------------------------------------------------------------- /section05/static_dirs/hello.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package hello 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | ) 20 | 21 | func helloHandler(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprintln(w, "Hello, Backend") 23 | } 24 | 25 | func init() { 26 | http.HandleFunc("/api/hello", helloHandler) 27 | } 28 | -------------------------------------------------------------------------------- /section01/examples/get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | "net/http" 20 | ) 21 | 22 | func main() { 23 | // try changing the value of this url 24 | res, err := http.Get("https://golang.org") 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | fmt.Println(res.Status) 29 | } 30 | -------------------------------------------------------------------------------- /events/step3/README.md: -------------------------------------------------------------------------------- 1 | # Step 3: adding weather with openweathermap.org 2 | 3 | So far, we have an application that is capable of storing events submitted 4 | through a web form, and display them back into the web page. 5 | 6 | Now we're going to add some cool functionality by retrieving the weather for 7 | the location where the event is taking place. To do so we need to fetch 8 | information from an external API, and we will use open openweathermap.org. 9 | 10 | You need to sign up to https://openweathermap.org to obtain an API key and replace the 11 | value of WEATHER_API_KEY in the app.yaml file. 12 | 13 | Then you'll implement the call to the API, preparing the request, decoding the 14 | response, and adding the resulting weather to the events. 15 | 16 | This will cause one API call per event per request, which is incredibly wasteful. 17 | But don't worry we'll fix that later, for now come back to the 18 | [instructions](../../section08/README.md#congratulations). 19 | -------------------------------------------------------------------------------- /section02/examples/step3/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | "net/http" 20 | ) 21 | 22 | func helloHandler(w http.ResponseWriter, r *http.Request) { 23 | fmt.Fprintln(w, "Hello, web") 24 | } 25 | 26 | func main() { 27 | http.HandleFunc("/hello", helloHandler) 28 | err := http.ListenAndServe("127.0.0.1:8080", nil) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /events/step0/README.md: -------------------------------------------------------------------------------- 1 | # Step 0: basic architecture 2 | 3 | The goal for this step is to define the two entry points to your web application. 4 | 5 | You can try running before you start coding to see the current behavior. 6 | 7 | $ dev_appserver.py . 8 | 9 | Once you've implemented this, visiting `localhost:8080` should display a list of conferences, 10 | and clicking "New Event" should not do anything. 11 | 12 | In order to deploy you'll need to first install and configure [gcloud](https://cloud.google.com/sdk/downloads): 13 | 14 | $ gcloud init 15 | 16 | _Note_: You do not need to set up any Google Compute Engine zone. 17 | 18 | Then simply run 19 | 20 | $ gcloud app deploy --version=step0 app.yaml 21 | 22 | And then visit https://step0.your-project-id.appspot.com or running 23 | 24 | $ gcloud app browse --version=step0 25 | 26 | This will display a basic Events page but also an alert will be alerted. That's fine, for now. 27 | 28 | Once you're done, let's go back to the [instructions](../../section05/README.md#congratulations). 29 | -------------------------------------------------------------------------------- /section05/static_dirs/static/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | Hello, App Engine 21 | 22 | 23 | 24 | 25 | 26 |

Hello, App Engine

27 |

The backend says:

28 | 29 | 30 | -------------------------------------------------------------------------------- /section05/mixed_content/hello.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | Hello, App Engine 21 | 22 | 23 | 24 |

Hello, App Engine

25 |

The backend says:

26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /section01/examples/do-get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | "net/http" 20 | ) 21 | 22 | func doGet() { 23 | req, err := http.NewRequest("GET", "https://golang.org", nil) 24 | if err != nil { 25 | log.Fatalf("could not create request: %v", err) 26 | } 27 | client := http.DefaultClient 28 | res, err := client.Do(req) 29 | if err != nil { 30 | log.Fatalf("http request failed: %v", err) 31 | } 32 | fmt.Println(res.Status) 33 | } 34 | -------------------------------------------------------------------------------- /section03/examples/texthandler/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | ) 20 | 21 | type textHandler struct { 22 | h http.HandlerFunc 23 | } 24 | 25 | func (t textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 26 | // Set the content type 27 | w.Header().Set("Content-Type", "text/plain") 28 | // Then call ServeHTTP in the decorated handler. 29 | t.h(w, r) 30 | } 31 | 32 | func helloHandler(w http.ResponseWriter, r *http.Request) { 33 | fmt.Fprintln(w, "hello") 34 | } 35 | 36 | func main() { 37 | http.Handle("/hello", textHandler{helloHandler}) 38 | http.ListenAndServe(":8080", nil) 39 | } 40 | -------------------------------------------------------------------------------- /section04/app/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package app 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | 20 | "google.golang.org/appengine" 21 | "google.golang.org/appengine/urlfetch" 22 | ) 23 | 24 | func init() { 25 | http.HandleFunc("/", handler) 26 | } 27 | 28 | func handler(w http.ResponseWriter, r *http.Request) { 29 | // first create a new context 30 | c := appengine.NewContext(r) 31 | // and use that context to create a new http client 32 | client := urlfetch.Client(c) 33 | 34 | // now we can use that http client as before 35 | res, err := client.Get("http://google.com") 36 | if err != nil { 37 | http.Error(w, fmt.Sprintf("could not get google: %v", err), http.StatusInternalServerError) 38 | return 39 | } 40 | fmt.Fprintf(w, "Got Google with status %s\n", res.Status) 41 | } 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Want to contribute? 2 | 3 | Great! First, read this page (including the small print at the end). 4 | 5 | ### Before you contribute 6 | 7 | Before we can use your code, you must sign the Google Individual Contributor 8 | License Agreement (CLA), which you can do online. The CLA is necessary mainly 9 | because you own the copyright to your changes, even after your contribution 10 | becomes part of our codebase, so we need your permission to use and distribute 11 | your code. We also need to be sure of various other things—for instance that 12 | you'll tell us if you know that your code infringes on other people's patents. 13 | You don't have to sign the CLA until after you've submitted your code for 14 | review and a member has approved it, but you must do it before we can put your 15 | code into our codebase. Before you start working on a larger contribution, you 16 | should get in touch with us first through the issue tracker with your idea so 17 | that we can help out and possibly guide you. Coordinating up front makes it 18 | much easier to avoid frustration later on. 19 | 20 | ### Code reviews 21 | 22 | All submissions, including submissions by project members, require review. 23 | We use Github pull requests for this purpose. 24 | 25 | ### The small print 26 | 27 | Contributions made by corporations are covered by a different agreement than 28 | the one above, the Software Grant and Corporate Contributor License Agreement. 29 | -------------------------------------------------------------------------------- /section03/examples/handlers/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "io/ioutil" 19 | "log" 20 | "net/http" 21 | ) 22 | 23 | func main() { 24 | http.HandleFunc("/body", bodyHandler) 25 | http.HandleFunc("/param", paramHandler) 26 | log.Fatal(http.ListenAndServe(":8080", nil)) 27 | } 28 | 29 | func bodyHandler(w http.ResponseWriter, r *http.Request) { 30 | b, err := ioutil.ReadAll(r.Body) 31 | if err != nil { 32 | fmt.Fprintf(w, "could not read body: %v", err) 33 | return 34 | } 35 | name := string(b) 36 | if name == "" { 37 | name = "friend" 38 | } 39 | fmt.Fprintf(w, "Hello, %s!", name) 40 | } 41 | 42 | func paramHandler(w http.ResponseWriter, r *http.Request) { 43 | name := r.FormValue("name") 44 | if name == "" { 45 | name = "friend" 46 | } 47 | fmt.Fprintf(w, "Hello, %s!", name) 48 | } 49 | -------------------------------------------------------------------------------- /section10/README.md: -------------------------------------------------------------------------------- 1 | # 10: Congratulations! 2 | 3 | You're done! Really!! You're now a cloud gopher! 🎉👏👍🎂👌 4 | 5 | Enjoy this gopher flying above the clouds. 6 | 7 | 8 | 9 | There's many things that you could do now: 10 | 11 | - Celebrate by closing your computer and going for a walk and some fresh air, you deserve it! 12 | - Learn more about [App Engine](https://cloud.google.com/appengine/docs/go/) and its extra features such as: 13 | - sending email with the [mail](https://cloud.google.com/appengine/docs/go/mail/) package, or [sendgrid](https://cloud.google.com/appengine/docs/go/mail/sendgrid), or [mailgun](https://cloud.google.com/appengine/docs/go/mail/mailgun) 14 | - setting up [authentication](https://cloud.google.com/appengine/docs/go/oauth/) 15 | - manipulating images with the [images API](https://cloud.google.com/appengine/docs/go/images/) 16 | - and many, many more 17 | - Tweet about how awesome this workshop was and mention me [@francesc](https://twitter.com/francesc) 18 | - If you like this repo you should put a 🌟 on it 19 | - Build your next application that will make you rich, and then give me a share of it (thanks ) 20 | 🤑) 21 | - Submit issues or fixes to this repo (thanks again) 22 | - Enjoy this animated ASCII train 🚅 powered by the [Go playground](https://play.golang.org/p/NOycgN2i6b) 23 | 24 | Thanks for taking the time of going through the workshop. I hope you enjoyed it! 25 | 26 | _Francesc_ 27 | -------------------------------------------------------------------------------- /section08/fetch/fetch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package fetch 15 | 16 | import ( 17 | "io" 18 | "net/http" 19 | 20 | "google.golang.org/appengine" 21 | "google.golang.org/appengine/log" 22 | "google.golang.org/appengine/urlfetch" 23 | ) 24 | 25 | func handler(w http.ResponseWriter, r *http.Request) { 26 | ctx := appengine.NewContext(r) 27 | 28 | // create a new HTTP client 29 | c := urlfetch.Client(ctx) 30 | 31 | // and use it to request the Google homepage 32 | res, err := c.Get("https://google.com") 33 | if err != nil { 34 | http.Error(w, err.Error(), http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | // we need to close the body at the end of this function 39 | defer res.Body.Close() 40 | 41 | // then we can dump the whole webpage onto our output 42 | _, err = io.Copy(w, res.Body) 43 | if err != nil { 44 | log.Errorf(ctx, "could not copy the response: %v", err) 45 | } 46 | } 47 | 48 | func init() { 49 | http.HandleFunc("/", handler) 50 | } 51 | -------------------------------------------------------------------------------- /section02/examples/gorilla.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "log" 18 | "net/http" 19 | 20 | "github.com/gorilla/mux" 21 | ) 22 | 23 | func listProducts(w http.ResponseWriter, r *http.Request) { 24 | // list all products 25 | } 26 | 27 | func addProduct(w http.ResponseWriter, r *http.Request) { 28 | // add a product 29 | } 30 | 31 | func getProduct(w http.ResponseWriter, r *http.Request) { 32 | id := mux.Vars(r)["productID"] 33 | log.Printf("fetching product with ID %q", id) 34 | // get a specific product 35 | } 36 | 37 | func main() { 38 | r := mux.NewRouter() 39 | // match only GET requests on /product/ 40 | r.HandleFunc("/product/", listProducts).Methods("GET") 41 | 42 | // match only POST requests on /product/ 43 | r.HandleFunc("/product/", addProduct).Methods("POST") 44 | 45 | // match GET regardless of productID 46 | r.HandleFunc("/product/{productID}", getProduct) 47 | 48 | // handle all requests with the Gorilla router. 49 | http.Handle("/", r) 50 | if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /events/step0/static/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | function EventsCtrl($scope, $http) { 17 | // The list of events in display. 18 | $scope.events = []; 19 | // The fields in the event creation dialog. 20 | $scope.newEvent = {}; 21 | 22 | // Display an error using an alert dialog. 23 | var alertError = function(data, status) { 24 | alert('code ' + status + ': ' + data); 25 | }; 26 | 27 | // Fetches all the events from the API. 28 | var fetchEvents = function() { 29 | return $http.get('/api/events'). 30 | error(alertError). 31 | success(function(data) { $scope.events = data; }); 32 | }; 33 | 34 | // Adds a new event throught the API. 35 | $scope.addEvent = function() { 36 | $http.post('/api/events', $scope.newEvent). 37 | error(alertError). 38 | success(function() { 39 | fetchEvents().then(function () { 40 | // If everything worked, clear the dialog. 41 | $scope.event = {}; 42 | // Fetch again after a bit, in case of eventual consistency. 43 | setTimeout(fetchEvents, 1000); 44 | }); 45 | }); 46 | }; 47 | 48 | // Fetch the list of events from the API. 49 | fetchEvents(); 50 | } 51 | -------------------------------------------------------------------------------- /events/step1/static/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | function EventsCtrl($scope, $http) { 17 | // The list of events in display. 18 | $scope.events = []; 19 | // The fields in the event creation dialog. 20 | $scope.newEvent = {}; 21 | 22 | // Display an error using an alert dialog. 23 | var alertError = function(data, status) { 24 | alert('code ' + status + ': ' + data); 25 | }; 26 | 27 | // Fetches all the events from the API. 28 | var fetchEvents = function() { 29 | return $http.get('/api/events'). 30 | error(alertError). 31 | success(function(data) { $scope.events = data; }); 32 | }; 33 | 34 | // Adds a new event throught the API. 35 | $scope.addEvent = function() { 36 | $http.post('/api/events', $scope.newEvent). 37 | error(alertError). 38 | success(function() { 39 | fetchEvents().then(function () { 40 | // If everything worked, clear the dialog. 41 | $scope.event = {}; 42 | // Fetch again after a bit, in case of eventual consistency. 43 | setTimeout(fetchEvents, 1000); 44 | }); 45 | }); 46 | }; 47 | 48 | // Fetch the list of events from the API. 49 | fetchEvents(); 50 | } 51 | -------------------------------------------------------------------------------- /events/step2/static/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | function EventsCtrl($scope, $http) { 17 | // The list of events in display. 18 | $scope.events = []; 19 | // The fields in the event creation dialog. 20 | $scope.newEvent = {}; 21 | 22 | // Display an error using an alert dialog. 23 | var alertError = function(data, status) { 24 | alert('code ' + status + ': ' + data); 25 | }; 26 | 27 | // Fetches all the events from the API. 28 | var fetchEvents = function() { 29 | return $http.get('/api/events'). 30 | error(alertError). 31 | success(function(data) { $scope.events = data; }); 32 | }; 33 | 34 | // Adds a new event throught the API. 35 | $scope.addEvent = function() { 36 | $http.post('/api/events', $scope.newEvent). 37 | error(alertError). 38 | success(function() { 39 | fetchEvents().then(function () { 40 | // If everything worked, clear the dialog. 41 | $scope.event = {}; 42 | // Fetch again after a bit, in case of eventual consistency. 43 | setTimeout(fetchEvents, 1000); 44 | }); 45 | }); 46 | }; 47 | 48 | // Fetch the list of events from the API. 49 | fetchEvents(); 50 | } 51 | -------------------------------------------------------------------------------- /events/step3/static/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | function EventsCtrl($scope, $http) { 17 | // The list of events in display. 18 | $scope.events = []; 19 | // The fields in the event creation dialog. 20 | $scope.newEvent = {}; 21 | 22 | // Display an error using an alert dialog. 23 | var alertError = function(data, status) { 24 | alert('code ' + status + ': ' + data); 25 | }; 26 | 27 | // Fetches all the events from the API. 28 | var fetchEvents = function() { 29 | return $http.get('/api/events'). 30 | error(alertError). 31 | success(function(data) { $scope.events = data; }); 32 | }; 33 | 34 | // Adds a new event throught the API. 35 | $scope.addEvent = function() { 36 | $http.post('/api/events', $scope.newEvent). 37 | error(alertError). 38 | success(function() { 39 | fetchEvents().then(function () { 40 | // If everything worked, clear the dialog. 41 | $scope.event = {}; 42 | // Fetch again after a bit, in case of eventual consistency. 43 | setTimeout(fetchEvents, 1000); 44 | }); 45 | }); 46 | }; 47 | 48 | // Fetch the list of events from the API. 49 | fetchEvents(); 50 | } 51 | -------------------------------------------------------------------------------- /events/step4/static/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | function EventsCtrl($scope, $http) { 17 | // The list of events in display. 18 | $scope.events = []; 19 | // The fields in the event creation dialog. 20 | $scope.newEvent = {}; 21 | 22 | // Display an error using an alert dialog. 23 | var alertError = function(data, status) { 24 | alert('code ' + status + ': ' + data); 25 | }; 26 | 27 | // Fetches all the events from the API. 28 | var fetchEvents = function() { 29 | return $http.get('/api/events'). 30 | error(alertError). 31 | success(function(data) { $scope.events = data; }); 32 | }; 33 | 34 | // Adds a new event throught the API. 35 | $scope.addEvent = function() { 36 | $http.post('/api/events', $scope.newEvent). 37 | error(alertError). 38 | success(function() { 39 | fetchEvents().then(function () { 40 | // If everything worked, clear the dialog. 41 | $scope.event = {}; 42 | // Fetch again after a bit, in case of eventual consistency. 43 | setTimeout(fetchEvents, 1000); 44 | }); 45 | }); 46 | }; 47 | 48 | // Fetch the list of events from the API. 49 | fetchEvents(); 50 | } 51 | -------------------------------------------------------------------------------- /events/step5/static/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | function EventsCtrl($scope, $http) { 17 | // The list of events in display. 18 | $scope.events = []; 19 | // The fields in the event creation dialog. 20 | $scope.newEvent = {}; 21 | 22 | // Display an error using an alert dialog. 23 | var alertError = function(data, status) { 24 | alert('code ' + status + ': ' + data); 25 | }; 26 | 27 | // Fetches all the events from the API. 28 | var fetchEvents = function() { 29 | return $http.get('/api/events'). 30 | error(alertError). 31 | success(function(data) { $scope.events = data; }); 32 | }; 33 | 34 | // Adds a new event throught the API. 35 | $scope.addEvent = function() { 36 | $http.post('/api/events', $scope.newEvent). 37 | error(alertError). 38 | success(function() { 39 | fetchEvents().then(function () { 40 | // If everything worked, clear the dialog. 41 | $scope.event = {}; 42 | // Fetch again after a bit, in case of eventual consistency. 43 | setTimeout(fetchEvents, 1000); 44 | }); 45 | }); 46 | }; 47 | 48 | // Fetch the list of events from the API. 49 | fetchEvents(); 50 | } 51 | -------------------------------------------------------------------------------- /section09/getset/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package app 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | "time" 20 | 21 | "google.golang.org/appengine" 22 | "google.golang.org/appengine/memcache" 23 | ) 24 | 25 | func set(w http.ResponseWriter, r *http.Request) { 26 | ctx := appengine.NewContext(r) 27 | 28 | // get the parameters k and v from the request 29 | key := r.FormValue("k") 30 | value := r.FormValue("v") 31 | 32 | item := &memcache.Item{ 33 | Key: key, 34 | Value: []byte(value), 35 | Expiration: 1 * time.Hour, 36 | } 37 | 38 | err := memcache.Set(ctx, item) 39 | if err != nil { 40 | http.Error(w, err.Error(), http.StatusInternalServerError) 41 | } 42 | } 43 | 44 | func get(w http.ResponseWriter, r *http.Request) { 45 | ctx := appengine.NewContext(r) 46 | 47 | key := r.FormValue("k") 48 | 49 | item, err := memcache.Get(ctx, key) 50 | switch err { 51 | case nil: 52 | fmt.Fprintf(w, "%s", item.Value) 53 | case memcache.ErrCacheMiss: 54 | fmt.Fprint(w, "key not found") 55 | default: 56 | http.Error(w, err.Error(), http.StatusInternalServerError) 57 | } 58 | } 59 | 60 | func init() { 61 | http.HandleFunc("/get", get) 62 | http.HandleFunc("/set", set) 63 | } 64 | -------------------------------------------------------------------------------- /events/step0/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | html, body { 17 | font-family: 'Roboto', sans-serif; 18 | width: 100%; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | h1 { 24 | text-align: center; 25 | background: #eee; 26 | padding: 30px; 27 | margin: 0 0 20px 0; 28 | box-shadow: 0 10px 20px #aaa; 29 | } 30 | 31 | .content { 32 | max-width: 800px; 33 | margin: auto; 34 | text-align: center; 35 | } 36 | 37 | .event, form { 38 | display: inline-block; 39 | width: 300px; 40 | box-shadow: 0 0 10px #333; 41 | border-radius: 10px; 42 | margin: 10px; 43 | } 44 | 45 | form { 46 | padding: 10px 0; 47 | background: #333; 48 | } 49 | 50 | form * { 51 | width: 80%; 52 | font-size: 0.8em; 53 | border: none; 54 | margin: 4px 10px; 55 | border-radius: 10px; 56 | text-align: center; 57 | } 58 | 59 | textarea { 60 | resize: vertical; 61 | } 62 | 63 | .title { 64 | display: block; 65 | text-align: center; 66 | margin: 10px; 67 | font-size: 1.5em; 68 | } 69 | 70 | .header { 71 | text-align: center; 72 | font-size: 0.8em; 73 | margin: 0; 74 | } 75 | 76 | .date { 77 | display: none; 78 | } 79 | 80 | .description { 81 | padding: 10px; 82 | line-height: 18px; 83 | } 84 | -------------------------------------------------------------------------------- /events/step1/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | html, body { 17 | font-family: 'Roboto', sans-serif; 18 | width: 100%; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | h1 { 24 | text-align: center; 25 | background: #eee; 26 | padding: 30px; 27 | margin: 0 0 20px 0; 28 | box-shadow: 0 10px 20px #aaa; 29 | } 30 | 31 | .content { 32 | max-width: 800px; 33 | margin: auto; 34 | text-align: center; 35 | } 36 | 37 | .event, form { 38 | display: inline-block; 39 | width: 300px; 40 | box-shadow: 0 0 10px #333; 41 | border-radius: 10px; 42 | margin: 10px; 43 | } 44 | 45 | form { 46 | padding: 10px 0; 47 | background: #333; 48 | } 49 | 50 | form * { 51 | width: 80%; 52 | font-size: 0.8em; 53 | border: none; 54 | margin: 4px 10px; 55 | border-radius: 10px; 56 | text-align: center; 57 | } 58 | 59 | textarea { 60 | resize: vertical; 61 | } 62 | 63 | .title { 64 | display: block; 65 | text-align: center; 66 | margin: 10px; 67 | font-size: 1.5em; 68 | } 69 | 70 | .header { 71 | text-align: center; 72 | font-size: 0.8em; 73 | margin: 0; 74 | } 75 | 76 | .date { 77 | display: none; 78 | } 79 | 80 | .description { 81 | padding: 10px; 82 | line-height: 18px; 83 | } 84 | -------------------------------------------------------------------------------- /events/step2/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | html, body { 17 | font-family: 'Roboto', sans-serif; 18 | width: 100%; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | h1 { 24 | text-align: center; 25 | background: #eee; 26 | padding: 30px; 27 | margin: 0 0 20px 0; 28 | box-shadow: 0 10px 20px #aaa; 29 | } 30 | 31 | .content { 32 | max-width: 800px; 33 | margin: auto; 34 | text-align: center; 35 | } 36 | 37 | .event, form { 38 | display: inline-block; 39 | width: 300px; 40 | box-shadow: 0 0 10px #333; 41 | border-radius: 10px; 42 | margin: 10px; 43 | } 44 | 45 | form { 46 | padding: 10px 0; 47 | background: #333; 48 | } 49 | 50 | form * { 51 | width: 80%; 52 | font-size: 0.8em; 53 | border: none; 54 | margin: 4px 10px; 55 | border-radius: 10px; 56 | text-align: center; 57 | } 58 | 59 | textarea { 60 | resize: vertical; 61 | } 62 | 63 | .title { 64 | display: block; 65 | text-align: center; 66 | margin: 10px; 67 | font-size: 1.5em; 68 | } 69 | 70 | .header { 71 | text-align: center; 72 | font-size: 0.8em; 73 | margin: 0; 74 | } 75 | 76 | .date { 77 | display: none; 78 | } 79 | 80 | .description { 81 | padding: 10px; 82 | line-height: 18px; 83 | } 84 | -------------------------------------------------------------------------------- /events/step3/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | html, body { 17 | font-family: 'Roboto', sans-serif; 18 | width: 100%; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | h1 { 24 | text-align: center; 25 | background: #eee; 26 | padding: 30px; 27 | margin: 0 0 20px 0; 28 | box-shadow: 0 10px 20px #aaa; 29 | } 30 | 31 | .content { 32 | max-width: 800px; 33 | margin: auto; 34 | text-align: center; 35 | } 36 | 37 | .event, form { 38 | display: inline-block; 39 | width: 300px; 40 | box-shadow: 0 0 10px #333; 41 | border-radius: 10px; 42 | margin: 10px; 43 | } 44 | 45 | form { 46 | padding: 10px 0; 47 | background: #333; 48 | } 49 | 50 | form * { 51 | width: 80%; 52 | font-size: 0.8em; 53 | border: none; 54 | margin: 4px 10px; 55 | border-radius: 10px; 56 | text-align: center; 57 | } 58 | 59 | textarea { 60 | resize: vertical; 61 | } 62 | 63 | .title { 64 | display: block; 65 | text-align: center; 66 | margin: 10px; 67 | font-size: 1.5em; 68 | } 69 | 70 | .header { 71 | text-align: center; 72 | font-size: 0.8em; 73 | margin: 0; 74 | } 75 | 76 | .date { 77 | display: none; 78 | } 79 | 80 | .description { 81 | padding: 10px; 82 | line-height: 18px; 83 | } 84 | -------------------------------------------------------------------------------- /events/step4/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | html, body { 17 | font-family: 'Roboto', sans-serif; 18 | width: 100%; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | h1 { 24 | text-align: center; 25 | background: #eee; 26 | padding: 30px; 27 | margin: 0 0 20px 0; 28 | box-shadow: 0 10px 20px #aaa; 29 | } 30 | 31 | .content { 32 | max-width: 800px; 33 | margin: auto; 34 | text-align: center; 35 | } 36 | 37 | .event, form { 38 | display: inline-block; 39 | width: 300px; 40 | box-shadow: 0 0 10px #333; 41 | border-radius: 10px; 42 | margin: 10px; 43 | } 44 | 45 | form { 46 | padding: 10px 0; 47 | background: #333; 48 | } 49 | 50 | form * { 51 | width: 80%; 52 | font-size: 0.8em; 53 | border: none; 54 | margin: 4px 10px; 55 | border-radius: 10px; 56 | text-align: center; 57 | } 58 | 59 | textarea { 60 | resize: vertical; 61 | } 62 | 63 | .title { 64 | display: block; 65 | text-align: center; 66 | margin: 10px; 67 | font-size: 1.5em; 68 | } 69 | 70 | .header { 71 | text-align: center; 72 | font-size: 0.8em; 73 | margin: 0; 74 | } 75 | 76 | .date { 77 | display: none; 78 | } 79 | 80 | .description { 81 | padding: 10px; 82 | line-height: 18px; 83 | } 84 | -------------------------------------------------------------------------------- /events/step5/static/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to writing, software distributed 9 | * under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. 11 | * 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | html, body { 17 | font-family: 'Roboto', sans-serif; 18 | width: 100%; 19 | padding: 0; 20 | margin: 0; 21 | } 22 | 23 | h1 { 24 | text-align: center; 25 | background: #eee; 26 | padding: 30px; 27 | margin: 0 0 20px 0; 28 | box-shadow: 0 10px 20px #aaa; 29 | } 30 | 31 | .content { 32 | max-width: 800px; 33 | margin: auto; 34 | text-align: center; 35 | } 36 | 37 | .event, form { 38 | display: inline-block; 39 | width: 300px; 40 | box-shadow: 0 0 10px #333; 41 | border-radius: 10px; 42 | margin: 10px; 43 | } 44 | 45 | form { 46 | padding: 10px 0; 47 | background: #333; 48 | } 49 | 50 | form * { 51 | width: 80%; 52 | font-size: 0.8em; 53 | border: none; 54 | margin: 4px 10px; 55 | border-radius: 10px; 56 | text-align: center; 57 | } 58 | 59 | textarea { 60 | resize: vertical; 61 | } 62 | 63 | .title { 64 | display: block; 65 | text-align: center; 66 | margin: 10px; 67 | font-size: 1.5em; 68 | } 69 | 70 | .header { 71 | text-align: center; 72 | font-size: 0.8em; 73 | margin: 0; 74 | } 75 | 76 | .date { 77 | display: none; 78 | } 79 | 80 | .description { 81 | padding: 10px; 82 | line-height: 18px; 83 | } 84 | -------------------------------------------------------------------------------- /events/step0/static/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Events 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Events

31 |
32 | 33 |
34 |
35 | {{e.title}} 36 |

{{e.weather}} in {{e.location}}

37 | {{e.date}} 38 |

{{e.description}} 

39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /events/step1/static/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Events 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Events

31 |
32 | 33 |
34 |
35 | {{e.title}} 36 |

{{e.weather}} in {{e.location}}

37 | {{e.date}} 38 |

{{e.description}} 

39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /events/step2/static/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Events 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Events

31 |
32 | 33 |
34 |
35 | {{e.title}} 36 |

{{e.weather}} in {{e.location}}

37 | {{e.date}} 38 |

{{e.description}} 

39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /events/step3/static/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Events 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Events

31 |
32 | 33 |
34 |
35 | {{e.title}} 36 |

{{e.weather}} in {{e.location}}

37 | {{e.date}} 38 |

{{e.description}} 

39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /events/step4/static/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Events 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Events

31 |
32 | 33 |
34 |
35 | {{e.title}} 36 |

{{e.weather}} in {{e.location}}

37 | {{e.date}} 38 |

{{e.description}} 

39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /events/step5/static/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Events 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |

Events

31 |
32 | 33 |
34 |
35 | {{e.title}} 36 |

{{e.weather}} in {{e.location}}

37 | {{e.date}} 38 |

{{e.description}} 

39 |
40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /section09/codec/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package app 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "net/http" 20 | "time" 21 | 22 | "google.golang.org/appengine" 23 | "google.golang.org/appengine/memcache" 24 | ) 25 | 26 | type Person struct { 27 | Name string `json:"name"` 28 | AgeYears int `json:"age_years"` 29 | } 30 | 31 | func set(w http.ResponseWriter, r *http.Request) { 32 | ctx := appengine.NewContext(r) 33 | 34 | var p Person 35 | if err := json.NewDecoder(r.Body).Decode(&p); err != nil { 36 | http.Error(w, err.Error(), http.StatusBadRequest) 37 | return 38 | } 39 | 40 | item := &memcache.Item{ 41 | Key: "last_person", 42 | Object: p, // we set the Object field instead of Value 43 | Expiration: 1 * time.Hour, 44 | } 45 | 46 | // we use the JSON codec 47 | err := memcache.JSON.Set(ctx, item) 48 | if err != nil { 49 | http.Error(w, err.Error(), http.StatusInternalServerError) 50 | } 51 | } 52 | 53 | func get(w http.ResponseWriter, r *http.Request) { 54 | ctx := appengine.NewContext(r) 55 | 56 | var p Person 57 | _, err := memcache.JSON.Get(ctx, "last_person", &p) 58 | if err == nil { 59 | json.NewEncoder(w).Encode(p) 60 | return 61 | } 62 | if err == memcache.ErrCacheMiss { 63 | fmt.Fprint(w, "key not found") 64 | return 65 | } 66 | http.Error(w, err.Error(), http.StatusInternalServerError) 67 | } 68 | 69 | func init() { 70 | http.HandleFunc("/get", get) 71 | http.HandleFunc("/set", set) 72 | } 73 | -------------------------------------------------------------------------------- /events/step0/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | func init() { 17 | // TODO: Create a new mux.Router 18 | // All the GET requests to /api/events should go to the handler listEvents. 19 | // All the POST requests to /api/events should go to the handler addEvent. 20 | // And all requests will be handled with the mux.Router. 21 | } 22 | 23 | // TODO: define a http.HandleFunc named listEvents 24 | // When called it should simply print the contents of the listOutput constant declared below. 25 | // You can also create a new App Engine context and use it to log some message. 26 | 27 | // TODO: define a http.HandleFunc named addEvent 28 | // When called it should simply set the status code to 201 and log a message. 29 | 30 | const listOutput = ` 31 | [ 32 | { 33 | "title": "Craft Conf", 34 | "description": "CRAFT is about software craftsmanship, which tools, methods, practices should be part of the toolbox of a modern developer and company.", 35 | "date": "2016-04-26T00:00:00Z", 36 | "location": "Budapest" 37 | }, 38 | { 39 | "title": "Google I/O", 40 | "description": "Google I/O is for developers - the creative coders who are building what's next. Each year, we explore the latest in tech, mobile \u0026 beyond.", 41 | "date": "2016-04-28T00:00:00Z", 42 | "location": "Mountain View" 43 | }, 44 | { 45 | "title": "GopherCon China", 46 | "description": "GOPHER'S BIGGEST PARTY", 47 | "date": "2016-05-18T00:00:00Z", 48 | "location": "Beijing" 49 | } 50 | ] 51 | ` 52 | -------------------------------------------------------------------------------- /events/step3/weather.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "errors" 18 | 19 | "golang.org/x/net/context" 20 | ) 21 | 22 | const ( 23 | apiURL = "http://api.openweathermap.org/data/2.5/weather" 24 | iconURLTemplate = "http://openweathermap.org/img/w/%s.png" 25 | ) 26 | 27 | func weather(ctx context.Context, location string) (*Weather, error) { 28 | // TODO: use apiURL above to fetch the weather for the location 29 | // You will need to create a map of URL parameters, known as url.Values from the "net/url" package. 30 | // Set the parameter "APPID" to the environment variable WEATHER_API_KEY defined in your app.yaml. 31 | // To access environment variables use the Getenv method from the os package. 32 | // Then set the parameter "q" to the location. 33 | 34 | // TODO: use urlfetch to create a client and call the API. 35 | // Dont' forget to close the Body of the response at the end of this function. 36 | 37 | // TODO: Create a variable with anonymous structure type containing the fields we care about. 38 | // We care about the first weather of the list of weathers, including its description and icon. 39 | // We also need the error message to understand if something went wrong. 40 | // See the examples in good_api_output.json and bad_api_output.json to undertand 41 | // how the values are encoded. 42 | 43 | // TODO: Create a json decoder reading from the response's body 44 | // and extract the information we care about. 45 | 46 | // TODO: check wheter the error message is empty, if not return an error with its contents. 47 | 48 | // Return the first element of the list of weathers we decoded. 49 | 50 | return nil, errors.New("weather not implemented") 51 | } 52 | -------------------------------------------------------------------------------- /events/step4/weather.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "net/url" 20 | "os" 21 | 22 | "golang.org/x/net/context" 23 | 24 | "google.golang.org/appengine/urlfetch" 25 | ) 26 | 27 | const ( 28 | apiURL = "http://api.openweathermap.org/data/2.5/weather" 29 | iconURLTemplate = "http://openweathermap.org/img/w/%s.png" 30 | ) 31 | 32 | func weather(ctx context.Context, location string) (*Weather, error) { 33 | // TODO: check if the weather for the location is in memcache. 34 | // If it is return it directly, if not just log the error if it is not a cache miss. 35 | 36 | // Prepare the request to the weather API. 37 | values := make(url.Values) 38 | values.Set("APPID", os.Getenv("WEATHER_API_KEY")) 39 | values.Set("q", location) 40 | url := apiURL + "?" + values.Encode() 41 | 42 | res, err := urlfetch.Client(ctx).Get(url) 43 | if err != nil { 44 | return nil, fmt.Errorf("could not get weather: %v", err) 45 | } 46 | 47 | // We need to close the body of the API response to avoid leaks. 48 | defer res.Body.Close() 49 | 50 | // We need to decode the list of weathers and the error message. 51 | var data struct { 52 | Weather []Weather 53 | Message string 54 | } 55 | if err := json.NewDecoder(res.Body).Decode(&data); err != nil { 56 | return nil, fmt.Errorf("could not decode weather: %v", err) 57 | } 58 | 59 | // If the error message is not empty, something bad happened. 60 | if data.Message != "" { 61 | return nil, fmt.Errorf("no weather found: %s", data.Message) 62 | } 63 | 64 | // Check whether we received any weather. 65 | if len(data.Weather) == 0 { 66 | return nil, fmt.Errorf("no weather found") 67 | } 68 | 69 | // We just take the first value for the weather. 70 | weather := data.Weather[0] 71 | // And make the icon a complete url. 72 | weather.Icon = fmt.Sprintf(iconURLTemplate, weather.Icon) 73 | 74 | // TODO: cache the weather in memcache for later. 75 | // If there's an error just log it and return the weather. 76 | 77 | return &weather, nil 78 | } 79 | -------------------------------------------------------------------------------- /section08/README.md: -------------------------------------------------------------------------------- 1 | # 8: Retrieving remote resources with urlfetch 2 | 3 | Sometimes your application will need to communicate with the external world, 4 | send data via POST requests or maybe retrieve some information using GET. 5 | 6 | The App Engine framework limits what you can do to ensure scalability and 7 | performance, which means you can't use the `net/http` package directly to 8 | run `http.Get("https://google.com")` but it's just as simple using the 9 | [`appengine/urlfetch`](https://cloud.google.com/appengine/docs/go/urlfetch/) 10 | package provided by the App Engine runtime: 11 | 12 | The most important function of the `urlfetch` package is `Client`: 13 | 14 | ```go 15 | func Client(context appengine.Context) *http.Client 16 | ``` 17 | 18 | So given an `appengine.Context` you get an HTTP client, and then you can start 19 | from there. So if you wanted to fetch Google's home page you would do: 20 | 21 | [embedmd]:# (fetch/fetch.go /package fetch/ /^}/) 22 | ```go 23 | package fetch 24 | 25 | import ( 26 | "io" 27 | "net/http" 28 | 29 | "google.golang.org/appengine" 30 | "google.golang.org/appengine/log" 31 | "google.golang.org/appengine/urlfetch" 32 | ) 33 | 34 | func handler(w http.ResponseWriter, r *http.Request) { 35 | ctx := appengine.NewContext(r) 36 | 37 | // create a new HTTP client 38 | c := urlfetch.Client(ctx) 39 | 40 | // and use it to request the Google homepage 41 | res, err := c.Get("https://google.com") 42 | if err != nil { 43 | http.Error(w, err.Error(), http.StatusInternalServerError) 44 | return 45 | } 46 | 47 | // we need to close the body at the end of this function 48 | defer res.Body.Close() 49 | 50 | // then we can dump the whole webpage onto our output 51 | _, err = io.Copy(w, res.Body) 52 | if err != nil { 53 | log.Errorf(ctx, "could not copy the response: %v", err) 54 | } 55 | } 56 | ``` 57 | 58 | If you run this code which you can find in this [directory](fetch) you should 59 | see a slightly broken version of the Google homepage, as all the local links 60 | will be broken. 61 | 62 | # Exercise: fetching weather for event locations 63 | 64 | With what you just learned and your previous knowledge on JSON encoding and 65 | decoding, it is time to tackle [step 3](../events/step3/README.md). 66 | 67 | # Congratulations! 68 | 69 | You are now capable of making your App Engine application communicate with the 70 | rest of the web using HTTP requests thanks to the `urlfetch` package! 71 | 72 | But is it really a good idea to fetch external resources every time we need to 73 | serve a request? Shouldn't we be caching some of this data for later use? 74 | 75 | Well, continue to the [next chapter](../section09/README.md) to learn how to 76 | cache and retrieve information from App Engine using memcache. 77 | -------------------------------------------------------------------------------- /section06/examples/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package app 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "log" 20 | "net/http" 21 | "os" 22 | ) 23 | 24 | // A sample JSON equivalent 25 | /* 26 | { 27 | "name": "gopher", 28 | "age_years": 5 29 | } 30 | */ 31 | 32 | // Person contains all the information I can imagine about a person right now. 33 | type Person struct { 34 | Name string `json:"name"` 35 | AgeYears int `json:"age_years"` 36 | } 37 | 38 | func encode() { 39 | p := Person{"gopher", 5} 40 | 41 | // create an encoder that will write on the standard output. 42 | enc := json.NewEncoder(os.Stdout) 43 | // use the encoder to encode p, which could fail. 44 | err := enc.Encode(p) 45 | // if it failed, log the error and stop execution. 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | } 50 | 51 | func decode() { 52 | // create an empty Person value. 53 | var p Person 54 | 55 | // create a decoder reading from the standard input. 56 | dec := json.NewDecoder(os.Stdin) 57 | // use the decoder to decode a value into p. 58 | err := dec.Decode(&p) 59 | // if it failed, log the error and stop execution. 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | // otherwise log what we decoded. 64 | fmt.Printf("decoded: %#v\n", p) 65 | } 66 | 67 | func encodeHandler(w http.ResponseWriter, r *http.Request) { 68 | p := Person{"gopher", 5} 69 | 70 | // set the Content-Type header. 71 | w.Header().Set("Content-Type", "application/json") 72 | 73 | // encode p to the output. 74 | enc := json.NewEncoder(w) 75 | err := enc.Encode(p) 76 | if err != nil { 77 | // if encoding fails, create an error page with code 500. 78 | http.Error(w, err.Error(), http.StatusInternalServerError) 79 | } 80 | } 81 | 82 | func decodeHandler(w http.ResponseWriter, r *http.Request) { 83 | var p Person 84 | 85 | dec := json.NewDecoder(r.Body) 86 | err := dec.Decode(&p) 87 | if err != nil { 88 | http.Error(w, err.Error(), http.StatusBadRequest) 89 | return 90 | } 91 | fmt.Fprintf(w, "Name is %v and age is %v", p.Name, p.AgeYears) 92 | } 93 | 94 | func init() { 95 | http.HandleFunc("/encode", encodeHandler) 96 | http.HandleFunc("/decode", decodeHandler) 97 | log.Fatal(http.ListenAndServe("localhost:8080", nil)) 98 | } 99 | -------------------------------------------------------------------------------- /events/step5/weather.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "net/url" 20 | "os" 21 | "time" 22 | 23 | "golang.org/x/net/context" 24 | 25 | "google.golang.org/appengine/log" 26 | "google.golang.org/appengine/memcache" 27 | "google.golang.org/appengine/urlfetch" 28 | ) 29 | 30 | const ( 31 | apiURL = "http://api.openweathermap.org/data/2.5/weather" 32 | iconURLTemplate = "http://openweathermap.org/img/w/%s.png" 33 | ) 34 | 35 | func weather(ctx context.Context, location string) (*Weather, error) { 36 | // check if the weather for the location is in memcache. 37 | var weather Weather 38 | _, err := memcache.JSON.Get(ctx, location, &weather) 39 | if err == nil { 40 | return &weather, nil 41 | } else if err != memcache.ErrCacheMiss { 42 | log.Errorf(ctx, "could not retrieve weather for %q from memcache: %v", location, err) 43 | } 44 | 45 | // Prepare the request to the weather API. 46 | values := make(url.Values) 47 | values.Set("APPID", os.Getenv("WEATHER_API_KEY")) 48 | values.Set("q", location) 49 | url := apiURL + "?" + values.Encode() 50 | 51 | res, err := urlfetch.Client(ctx).Get(url) 52 | if err != nil { 53 | return nil, fmt.Errorf("could not get weather: %v", err) 54 | } 55 | 56 | // We need to close the body of the API response to avoid leaks. 57 | defer res.Body.Close() 58 | 59 | // We need to decode the list of weathers and the error message. 60 | var data struct { 61 | Weather []Weather 62 | Message string 63 | } 64 | if err := json.NewDecoder(res.Body).Decode(&data); err != nil { 65 | return nil, fmt.Errorf("could not decode weather: %v", err) 66 | } 67 | 68 | // If the error message is not empty, something bad happened. 69 | if data.Message != "" { 70 | return nil, fmt.Errorf("no weather found: %s", data.Message) 71 | } 72 | 73 | // Check whether we received any weather. 74 | if len(data.Weather) == 0 { 75 | return nil, fmt.Errorf("no weather found") 76 | } 77 | 78 | // We just take the first value for the weather. 79 | weather = data.Weather[0] 80 | // And make the icon a complete url. 81 | weather.Icon = fmt.Sprintf(iconURLTemplate, weather.Icon) 82 | 83 | // Cache the weather in memcache for later. 84 | item := &memcache.Item{ 85 | Key: location, 86 | Object: &weather, 87 | Expiration: 1 * time.Hour, 88 | } 89 | if err := memcache.JSON.Set(ctx, item); err != nil { 90 | log.Errorf(ctx, "could not cache weather for %q: %v", location, err) 91 | } 92 | 93 | return &weather, nil 94 | } 95 | -------------------------------------------------------------------------------- /section00/README.md: -------------------------------------------------------------------------------- 1 | # 0: Hello world 2 | 3 | Before we start writing web servers let's analyze a simple Go program. 4 | 5 | [embedmd]:# (hello/main.go /package main/ $) 6 | ```go 7 | package main 8 | 9 | import "fmt" 10 | 11 | func main() { 12 | fmt.Println("hello, world!") } 13 | ``` 14 | 15 | You should be able to understand every line of this program. 16 | If you do not it's probably time to check out the [Go tour][1]. 17 | 18 | ## Running your code 19 | 20 | At this point you can paste the code above into a file named `main.go`. 21 | This file should be anywhere in your `$GOPATH`. 22 | I will assume it is inside of a folder with path `$GOPATH/src/hello/main.go`. 23 | 24 | You can now run the code in a couple of ways. From the directory containing `main.go` run: 25 | 26 | - `go run main.go` compiles and runs only `main.go`. 27 | 28 | - `go install` compiles the code in the current directory and generates a binary named `hello` in `$GOPATH/bin`. 29 | 30 | ## Formatting your code: gofmt 31 | 32 | If you're used to seeing some Go code you might have noticed that this code is not formatted in a standard way. 33 | 34 | Try running `gofmt main.go` and you'll see what the output should look like. 35 | You can run `gofmt -d main.go` to see how the file differs from the formatted version. 36 | And finally you can run `gofmt -w main.go` to simply rewrite the file with its formatted version. 37 | 38 | ## Managing import statements: goimports 39 | 40 | The import statements at the beginning of the program are needed for the compiler to know what packages you use. 41 | But this doesn't mean you need to write them yourself! 42 | 43 | Try deleting the line `import "fmt"` from `main.go` and run it, you should see an error: 44 | 45 | ```bash 46 | $ go run main.go 47 | # command-line-arguments 48 | ./main.go:5: undefined: fmt in fmt.Println 49 | ``` 50 | 51 | You can fix this error by manually adding the missing import statements or using `goimports`. 52 | 53 | If you don't have `goimports` installed in your machine you can easily install it by running: 54 | 55 | ```bash 56 | $ go get golang.org/x/tools/cmd/goimports 57 | ``` 58 | 59 | This will install the `goimports` binary in `$GOAPTH/bin`. 60 | It is basically the equivalent to fetching the code from GitHub and then running `go install`. 61 | 62 | You can now run `goimports` as a replacement of `gofmt`. 63 | `goimports` formats your code *and* fixes your imported package list adding missing ones and removing those unused. 64 | 65 | Similarly to `gofmt` you can run: 66 | 67 | - `goimports main.go` to see what the fixed file would look like. 68 | - `goimports -d main.go` to see the difference between the current and fixed versions. 69 | - `goimports -w main.go` to rewrite the file with its fixed version. 70 | 71 | ## Making your life easier 72 | 73 | `gofmt` and `goimports` are just two of the tools that you might use to make your life easier. 74 | To have those and more invoked everytime you save your file you should consider adding a plugin to your favorite editor. 75 | I personally love [vim][2] with [vim-go][3] and I'm also using [VS Code][4] with its Go plugin. 76 | But there's many more editors with Go support, you can find them [here][5]. 77 | 78 | # Congratulations! 79 | 80 | You're done with section ... zero. Well, we need to start somewhere! 81 | But hey, at least now you're ready to tackle [section 1][6]. 82 | 83 | [1]: https://tour.golang.org 84 | [2]: https://www.vim.org/ 85 | [3]: https://github.com/fatih/vim-go 86 | [4]: https://www.visualstudio.com/en-us/products/code-vs.aspx 87 | [5]: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins 88 | [6]: ../section01/README.md 89 | -------------------------------------------------------------------------------- /events/step2/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "time" 22 | 23 | "google.golang.org/appengine" 24 | "google.golang.org/appengine/log" 25 | 26 | "github.com/gorilla/mux" 27 | ) 28 | 29 | const eventKind = "Event" 30 | 31 | // Event contains the information related to an event. 32 | type Event struct { 33 | Title string `json:"title"` 34 | Description string `json:"description"` 35 | Date time.Time `json:"date"` 36 | Location string `json:"location"` 37 | } 38 | 39 | func init() { 40 | r := mux.NewRouter() 41 | r.HandleFunc("/api/events", listEvents).Methods("GET") 42 | r.HandleFunc("/api/events", addEvent).Methods("POST") 43 | http.Handle("/", r) 44 | } 45 | 46 | func listEvents(w http.ResponseWriter, r *http.Request) { 47 | ctx := appengine.NewContext(r) 48 | 49 | var events []Event 50 | 51 | // TODO: Create a new context from the http request 52 | // Create a new query on the kind event. 53 | // Filter all the events that are in the past. 54 | // Order the events by date. 55 | // And limit so only 5 events are shown. 56 | 57 | // TODO: Use the GetAll method to fetch all the events into the slice of Events. 58 | // Check for any errors. 59 | 60 | w.Header().Set("Content-Type", "application/json") 61 | if err := json.NewEncoder(w).Encode(events); err != nil { 62 | log.Errorf(ctx, "encoding events: %v", err) 63 | } 64 | } 65 | 66 | func addEvent(w http.ResponseWriter, r *http.Request) { 67 | ctx := appengine.NewContext(r) 68 | 69 | e, err := decodeEvent(r.Body) 70 | if err != nil { 71 | http.Error(w, err.Error(), http.StatusBadRequest) 72 | return 73 | } 74 | 75 | log.Infof(ctx, "event decoded: %v", e) 76 | 77 | // TODO: Create a new incomplete key of type Event. 78 | // And use it to store the decoded event. 79 | // Remember to handle the error! 80 | http.Error(w, "add event not implemented", http.StatusNotImplemented) 81 | return 82 | 83 | w.WriteHeader(http.StatusCreated) 84 | } 85 | 86 | func decodeEvent(r io.Reader) (*Event, error) { 87 | // Using an anonymous type instead of Event because the date format 88 | // we need to parse is not standard. 89 | // You can find more on this in this talk: https://talks.golang.org/2015/json.slide 90 | var data struct { 91 | Title string 92 | Date string 93 | Location string 94 | Description string 95 | } 96 | 97 | err := json.NewDecoder(r).Decode(&data) 98 | if err != nil { 99 | return nil, fmt.Errorf("could not decode JSON: %v", err) 100 | } 101 | 102 | if data.Title == "" { 103 | return nil, fmt.Errorf("title is required") 104 | } 105 | if data.Location == "" { 106 | return nil, fmt.Errorf("location is required") 107 | } 108 | t, err := time.Parse("2006-01-02", data.Date) 109 | if err != nil { 110 | return nil, fmt.Errorf("could not parse date: %v", err) 111 | } 112 | 113 | return &Event{ 114 | Title: data.Title, 115 | Date: t, 116 | Description: data.Description, 117 | Location: data.Location, 118 | }, nil 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/campoy/go-web-workshop.svg)](https://travis-ci.org/campoy/go-web-workshop) [![Go Report Card](https://goreportcard.com/badge/github.com/campoy/go-web-workshop)](https://goreportcard.com/report/github.com/campoy/go-web-workshop) 2 | 3 | # Building Web Applications with Go 4 | 5 | Welcome, gopher! You're not a gopher? 6 | Well, this workshop is for gophers, or people that use the [Go programming language][1]. 7 | But fear not if you've never written any Go before! 8 | I'd recommend you learn the basics for the language first with the [Go tour][2]. 9 | 10 | This workshop has been run a couple of times with an instructor leading. The goal of 11 | this repo is to make it as easy as possible for individuals to follow the content 12 | by themselves. If you get stuck at any point, feel free to file issues asking questions. 13 | 14 | ## Setting up your workspace 15 | 16 | To go through this you will need the following: 17 | 18 | 1. You have installed the [Go Programming Language][1]. 19 | 1. You have set up a `GOPATH` by following the [How to Write Go Code][9] tutorial. 20 | 1. You are somewhat familiar with the basics of Go. (The [Go Tour][2] is a pretty good place to start) 21 | 1. You have a Google account and you have installed the [Google Cloud SDK][3]. 22 | 23 | ## Contents 24 | 25 | There's a lot to say about how to build web applications, in Go or any other language. 26 | But we only have one day so we won't try to cover too much. 27 | Instead we'll cover the basics, so you'll be able to explore other solutions and frameworks later. 28 | 29 | The workshops is divided in eleven sections: 30 | 31 | - [0: Hello world](section00/README.md) 32 | - [1: Web Clients](section01/README.md) 33 | - [2: Web servers](section02/README.md) 34 | - [3: Input validation and status codes](section03/README.md) 35 | - [4: Deploying to App Engine](section04/README.md) 36 | - [5: Hello, HTML](section05/README.md) 37 | - [6: JSON encoding and decoding](section06/README.md) 38 | - [7: Durable storage with Cloud Datastore](section07/README.md) 39 | - [8: Retrieving remote resources with urlfetch](section08/README.md) 40 | - [9: What is Memcache and how to use it from App Engine](section09/README.md) 41 | - [10: Congratulations!](section10/README.md) 42 | 43 | ## Resources 44 | 45 | These are places where you can find more information for Go: 46 | 47 | - [golang.org](https://golang.org) 48 | - [godoc.org](https://godoc.org), where you can find the documentation for any package. 49 | - [The Go Programming Language Blog](https://blog.golang.org) 50 | 51 | My favorite aspect of Go is its community, and you are now part of it too. Welcome! 52 | 53 | As a newcomer to the Go community you might have questions or get blocked at some point. 54 | This is completely normal, and we're here to help you. 55 | Some of the places where gophers tend to hang out are: 56 | 57 | - [The Go Forum](https://forum.golangbridge.org/) 58 | - #go-nuts IRC channel at [Freenode](https://freenode.net/) 59 | - Gophers’ community on [Slack](https://gophers.slack.com/messages/general/) (signup [here](https://invite.slack.golangbridge.org/) for an account). 60 | - [@golang](https://twitter.com/golang) and [#golang](https://twitter.com/search?q=%23golang) on Twitter. 61 | - [Go+ community](https://plus.google.com/u/1/communities/114112804251407510571) on Google Plus. 62 | - [Go user meetups](https://go-meetups.appspot.com/) 63 | - golang-nuts [mailing list](https://groups.google.com/forum/?fromgroups#!forum/golang-nuts) 64 | - Go community [Wiki](https://github.com/golang/go/wiki) 65 | 66 | ### Disclaimer 67 | 68 | This is not an official Google product (experimental or otherwise), it is just 69 | code that happens to be owned by Google. 70 | 71 | [1]: https://golang.org 72 | [2]: https://tour.golang.org 73 | [3]: https://cloud.google.com/sdk/downloads 74 | [9]: https://golang.org/doc/code.html#Organization 75 | -------------------------------------------------------------------------------- /section07/examples/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package app 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | 20 | "google.golang.org/appengine" 21 | "google.golang.org/appengine/datastore" 22 | ) 23 | 24 | // Person contains the name and age of a person. 25 | type Person struct { 26 | Name string 27 | AgeYears int 28 | } 29 | 30 | func completeHandler(w http.ResponseWriter, r *http.Request) { 31 | // create a new App Engine context from the HTTP request. 32 | ctx := appengine.NewContext(r) 33 | 34 | p := &Person{Name: "gopher", AgeYears: 5} 35 | 36 | // create a new complete key of kind Person and value gopher. 37 | key := datastore.NewKey(ctx, "Person", "gopher", 0, nil) 38 | // put p in the datastore. 39 | key, err := datastore.Put(ctx, key, p) 40 | if err != nil { 41 | http.Error(w, err.Error(), http.StatusInternalServerError) 42 | return 43 | } 44 | fmt.Fprintf(w, "gopher stored with key %v", key) 45 | } 46 | 47 | func incompleteHandler(w http.ResponseWriter, r *http.Request) { 48 | // create a new App Engine context from the HTTP request. 49 | ctx := appengine.NewContext(r) 50 | 51 | p := &Person{Name: "gopher", AgeYears: 5} 52 | 53 | // create a new complete key of kind Person. 54 | key := datastore.NewIncompleteKey(ctx, "Person", nil) 55 | // put p in the datastore. 56 | key, err := datastore.Put(ctx, key, p) 57 | if err != nil { 58 | http.Error(w, err.Error(), http.StatusInternalServerError) 59 | return 60 | } 61 | fmt.Fprintf(w, "gopher stored with key %v", key) 62 | } 63 | 64 | func getHandler(w http.ResponseWriter, r *http.Request) { 65 | ctx := appengine.NewContext(r) 66 | 67 | key := datastore.NewKey(ctx, "Person", "gopher", 0, nil) 68 | 69 | var p Person 70 | err := datastore.Get(ctx, key, &p) 71 | if err != nil { 72 | http.Error(w, "Person not found", http.StatusNotFound) 73 | return 74 | } 75 | fmt.Fprintln(w, p) 76 | } 77 | 78 | func queryHandler(w http.ResponseWriter, r *http.Request) { 79 | ctx := appengine.NewContext(r) 80 | 81 | var p []Person 82 | 83 | // create a new query on the kind Person 84 | q := datastore.NewQuery("Person") 85 | 86 | // select only values where field Age is 10 or lower 87 | q = q.Filter("Age <=", 10) 88 | 89 | // order all the values by the Name field 90 | q = q.Order("Name") 91 | 92 | // and finally execute the query retrieving all values into p. 93 | _, err := q.GetAll(ctx, &p) 94 | if err != nil { 95 | http.Error(w, err.Error(), http.StatusInternalServerError) 96 | return 97 | } 98 | fmt.Fprintln(w, p) 99 | } 100 | 101 | func chainedQueryHandler(w http.ResponseWriter, r *http.Request) { 102 | ctx := appengine.NewContext(r) 103 | 104 | var p []Person 105 | 106 | // create a new query on the kind Person 107 | q := datastore.NewQuery("Person"). 108 | Filter("Age <=", 10). 109 | Order("Name") 110 | 111 | // and finally execute the query retrieving all values into p. 112 | _, err := q.GetAll(ctx, &p) 113 | if err != nil { 114 | // handle the error 115 | } 116 | } 117 | 118 | func init() { 119 | http.HandleFunc("/complete", completeHandler) 120 | http.HandleFunc("/incomplete", incompleteHandler) 121 | http.HandleFunc("/query", queryHandler) 122 | http.HandleFunc("/chainedQuery", chainedQueryHandler) 123 | http.HandleFunc("/get", getHandler) 124 | } 125 | -------------------------------------------------------------------------------- /events/step3/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "time" 22 | 23 | "google.golang.org/appengine" 24 | "google.golang.org/appengine/datastore" 25 | "google.golang.org/appengine/log" 26 | 27 | "github.com/gorilla/mux" 28 | ) 29 | 30 | const eventKind = "Event" 31 | 32 | // Event contains the information related to an event. 33 | type Event struct { 34 | Title string `json:"title"` 35 | Description string `json:"description"` 36 | Date time.Time `json:"date"` 37 | Location string `json:"location"` 38 | Weather *Weather `json:"weather" datastore:"-"` 39 | } 40 | 41 | // Weather contains the description and icon for a weather condition. 42 | type Weather struct { 43 | Description string `json:"description"` 44 | Icon string `json:"icon"` 45 | } 46 | 47 | func init() { 48 | r := mux.NewRouter() 49 | r.HandleFunc("/api/events", listEvents).Methods("GET") 50 | r.HandleFunc("/api/events", addEvent).Methods("POST") 51 | http.Handle("/", r) 52 | } 53 | 54 | func listEvents(w http.ResponseWriter, r *http.Request) { 55 | ctx := appengine.NewContext(r) 56 | events := []Event{} 57 | q := datastore.NewQuery(eventKind). 58 | Filter("Date >", time.Now()). 59 | Order("Date"). 60 | Limit(5) 61 | 62 | if _, err := q.GetAll(ctx, &events); err != nil { 63 | http.Error(w, err.Error(), http.StatusInternalServerError) 64 | return 65 | } 66 | 67 | // TODO: iterate over all the events and fetch the weather for its location. 68 | // If that operation fails just log the error and continue with the next events. 69 | // If it doesn't fail modify the weather in the event. 70 | 71 | w.Header().Set("Content-Type", "application/json") 72 | if err := json.NewEncoder(w).Encode(events); err != nil { 73 | log.Errorf(ctx, "encoding events: %v", err) 74 | } 75 | } 76 | 77 | func addEvent(w http.ResponseWriter, r *http.Request) { 78 | ctx := appengine.NewContext(r) 79 | 80 | e, err := decodeEvent(r.Body) 81 | if err != nil { 82 | http.Error(w, err.Error(), http.StatusBadRequest) 83 | return 84 | } 85 | 86 | key := datastore.NewIncompleteKey(ctx, eventKind, nil) 87 | if _, err := datastore.Put(ctx, key, e); err != nil { 88 | http.Error(w, err.Error(), http.StatusInternalServerError) 89 | return 90 | } 91 | 92 | w.WriteHeader(http.StatusCreated) 93 | } 94 | 95 | func decodeEvent(r io.Reader) (*Event, error) { 96 | // Using an anonymous type instead of Event because the date format 97 | // we need to parse is not standard. 98 | // You can find more on this in this talk: https://talks.golang.org/2015/json.slide 99 | var data struct { 100 | Title string 101 | Date string 102 | Location string 103 | Description string 104 | } 105 | 106 | err := json.NewDecoder(r).Decode(&data) 107 | if err != nil { 108 | return nil, fmt.Errorf("could not decode JSON: %v", err) 109 | } 110 | 111 | if data.Title == "" { 112 | return nil, fmt.Errorf("title is required") 113 | } 114 | if data.Location == "" { 115 | return nil, fmt.Errorf("location is required") 116 | } 117 | t, err := time.Parse("2006-01-02", data.Date) 118 | if err != nil { 119 | return nil, fmt.Errorf("could not parse date: %v", err) 120 | } 121 | 122 | return &Event{ 123 | Title: data.Title, 124 | Date: t, 125 | Description: data.Description, 126 | Location: data.Location, 127 | }, nil 128 | } 129 | -------------------------------------------------------------------------------- /events/step4/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "time" 22 | 23 | "google.golang.org/appengine" 24 | "google.golang.org/appengine/datastore" 25 | "google.golang.org/appengine/log" 26 | 27 | "github.com/gorilla/mux" 28 | ) 29 | 30 | const eventKind = "Event" 31 | 32 | // Event contains the information related to an event. 33 | type Event struct { 34 | Title string `json:"title"` 35 | Description string `json:"description"` 36 | Date time.Time `json:"date"` 37 | Location string `json:"location"` 38 | Weather *Weather `json:"weather" datastore:"-"` 39 | } 40 | 41 | // Weather contains the description and icon for a weather condition. 42 | type Weather struct { 43 | Description string `json:"description"` 44 | Icon string `json:"icon"` 45 | } 46 | 47 | func init() { 48 | r := mux.NewRouter() 49 | r.HandleFunc("/api/events", listEvents).Methods("GET") 50 | r.HandleFunc("/api/events", addEvent).Methods("POST") 51 | http.Handle("/", r) 52 | } 53 | 54 | func listEvents(w http.ResponseWriter, r *http.Request) { 55 | ctx := appengine.NewContext(r) 56 | events := []Event{} 57 | q := datastore.NewQuery(eventKind). 58 | Filter("Date >", time.Now()). 59 | Order("Date"). 60 | Limit(5) 61 | 62 | if _, err := q.GetAll(ctx, &events); err != nil { 63 | http.Error(w, err.Error(), http.StatusInternalServerError) 64 | return 65 | } 66 | 67 | for i, e := range events { 68 | w, err := weather(ctx, e.Location) 69 | if err != nil { 70 | log.Errorf(ctx, "fetching weather for %q: %v", e.Location, err) 71 | continue 72 | } 73 | events[i].Weather = w 74 | } 75 | 76 | w.Header().Set("Content-Type", "application/json") 77 | if err := json.NewEncoder(w).Encode(events); err != nil { 78 | log.Errorf(ctx, "encoding events: %v", err) 79 | } 80 | } 81 | 82 | func addEvent(w http.ResponseWriter, r *http.Request) { 83 | ctx := appengine.NewContext(r) 84 | 85 | e, err := decodeEvent(r.Body) 86 | if err != nil { 87 | http.Error(w, err.Error(), http.StatusBadRequest) 88 | return 89 | } 90 | 91 | key := datastore.NewIncompleteKey(ctx, eventKind, nil) 92 | if _, err := datastore.Put(ctx, key, e); err != nil { 93 | http.Error(w, err.Error(), http.StatusInternalServerError) 94 | return 95 | } 96 | 97 | w.WriteHeader(http.StatusCreated) 98 | } 99 | 100 | func decodeEvent(r io.Reader) (*Event, error) { 101 | // Using an anonymous type instead of Event because the date format 102 | // we need to parse is not standard. 103 | // You can find more on this in this talk: https://talks.golang.org/2015/json.slide 104 | var data struct { 105 | Title string 106 | Date string 107 | Location string 108 | Description string 109 | } 110 | 111 | err := json.NewDecoder(r).Decode(&data) 112 | if err != nil { 113 | return nil, fmt.Errorf("could not decode JSON: %v", err) 114 | } 115 | 116 | if data.Title == "" { 117 | return nil, fmt.Errorf("title is required") 118 | } 119 | if data.Location == "" { 120 | return nil, fmt.Errorf("location is required") 121 | } 122 | t, err := time.Parse("2006-01-02", data.Date) 123 | if err != nil { 124 | return nil, fmt.Errorf("could not parse date: %v", err) 125 | } 126 | 127 | return &Event{ 128 | Title: data.Title, 129 | Date: t, 130 | Description: data.Description, 131 | Location: data.Location, 132 | }, nil 133 | } 134 | -------------------------------------------------------------------------------- /events/step1/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "io" 18 | "net/http" 19 | "sync" 20 | "time" 21 | 22 | "google.golang.org/appengine" 23 | "google.golang.org/appengine/log" 24 | 25 | "github.com/gorilla/mux" 26 | ) 27 | 28 | // Event contains the information related to an event. 29 | type Event struct { 30 | Title string `json:"title"` 31 | Description string `json:"description"` 32 | Date time.Time `json:"date"` 33 | Location string `json:"location"` 34 | } 35 | 36 | var ( 37 | // you can remove these values if you prefer 38 | // they're here just in case you implement listEvents before addEvent. 39 | events = []Event{ 40 | { 41 | Title: "Craft Conf", 42 | Description: "CRAFT is about software craftsmanship, which tools, methods, practices should be part of the toolbox of a modern developer and company.", 43 | Date: time.Date(2016, 4, 26, 0, 0, 0, 0, time.Local), 44 | Location: "Budapest", 45 | }, 46 | { 47 | Title: "Google I/O", 48 | Description: "Google I/O is for developers - the creative coders who are building what's next. Each year, we explore the latest in tech, mobile \u0026 beyond.", 49 | Date: time.Date(2016, 5, 28, 0, 0, 0, 0, time.Local), 50 | Location: "Mountain View", 51 | }, 52 | { 53 | Title: "GopherCon China", 54 | Description: "GOPHER'S BIGGEST PARTY", 55 | Date: time.Date(2016, 5, 18, 0, 0, 0, 0, time.Local), 56 | Location: "Beijing", 57 | }, 58 | } 59 | mu sync.RWMutex 60 | ) 61 | 62 | func init() { 63 | r := mux.NewRouter() 64 | r.HandleFunc("/api/events", listEvents).Methods("GET") 65 | r.HandleFunc("/api/events", addEvent).Methods("POST") 66 | http.Handle("/", r) 67 | } 68 | 69 | func listEvents(w http.ResponseWriter, r *http.Request) { 70 | // TODO: Create a new context from the http request. 71 | // Grab a read lock from the global mutex and remember to release it at the end. 72 | // Set the header "Content-Type"" on the response to "application/json". 73 | // Create a json encoder that will write to the response writer. 74 | // And use it to encode the list of events. 75 | // Handle the error and log with log.Errorf if not nil. 76 | 77 | http.Error(w, "listEvents not implemented", http.StatusNotImplemented) 78 | } 79 | 80 | func addEvent(w http.ResponseWriter, r *http.Request) { 81 | ctx := appengine.NewContext(r) 82 | 83 | e, err := decodeEvent(r.Body) 84 | if err != nil { 85 | http.Error(w, err.Error(), http.StatusBadRequest) 86 | return 87 | } 88 | 89 | log.Infof(ctx, "event decoded: %+v", e) 90 | 91 | // TODO: grab a write lock from the global mutex and release it at the end. 92 | // Add the decoded event to the global list of events. 93 | // Set the status code of the response to 201. 94 | http.Error(w, "addEvent not implemented", http.StatusNotImplemented) 95 | } 96 | 97 | func decodeEvent(r io.Reader) (*Event, error) { 98 | var data struct { 99 | Title string 100 | Date string 101 | Location string 102 | Description string 103 | } 104 | 105 | // TODO: create a json decoder that will decode from the parameter io.Reader. 106 | // Use it to decode into data, and handle the error. 107 | 108 | // TODO: If the Title is empty return an error, you can use fmt.Errorf or errors.New. 109 | // If the Location is empty return an error. 110 | // Parse the Date using the format "2006-01-02", return an error if it is not valid. 111 | 112 | return &Event{ 113 | Title: data.Title, 114 | // TODO: assign the rest of values here. 115 | }, nil 116 | } 117 | -------------------------------------------------------------------------------- /events/step5/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package events 15 | 16 | import ( 17 | "encoding/json" 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "os" 22 | "time" 23 | 24 | "google.golang.org/appengine" 25 | "google.golang.org/appengine/datastore" 26 | "google.golang.org/appengine/log" 27 | 28 | "github.com/gorilla/mux" 29 | ) 30 | 31 | const eventKind = "Event" 32 | 33 | // Event contains the information related to an event. 34 | type Event struct { 35 | Title string `json:"title"` 36 | Description string `json:"description"` 37 | Date time.Time `json:"date"` 38 | Location string `json:"location"` 39 | Weather *Weather `json:"weather" datastore:"-"` 40 | } 41 | 42 | // Weather contains the description and icon for a weather condition. 43 | type Weather struct { 44 | Description string `json:"description"` 45 | Icon string `json:"icon"` 46 | } 47 | 48 | func init() { 49 | r := mux.NewRouter() 50 | r.HandleFunc("/api/events", listEvents).Methods("GET") 51 | r.HandleFunc("/api/events", addEvent).Methods("POST") 52 | http.Handle("/", r) 53 | } 54 | 55 | func listEvents(w http.ResponseWriter, r *http.Request) { 56 | ctx := appengine.NewContext(r) 57 | events := []Event{} 58 | q := datastore.NewQuery(eventKind). 59 | Filter("Date >", time.Now()). 60 | Order("Date"). 61 | Limit(5) 62 | 63 | if _, err := q.GetAll(ctx, &events); err != nil { 64 | http.Error(w, err.Error(), http.StatusInternalServerError) 65 | return 66 | } 67 | 68 | for i, e := range events { 69 | w, err := weather(ctx, e.Location) 70 | if err != nil { 71 | log.Errorf(ctx, "fetching weather for %q: %v", e.Location, err) 72 | continue 73 | } 74 | events[i].Weather = w 75 | } 76 | 77 | w.Header().Set("Content-Type", "application/json") 78 | if err := json.NewEncoder(w).Encode(events); err != nil { 79 | log.Errorf(ctx, "encoding events: %v", err) 80 | } 81 | } 82 | 83 | func addEvent(w http.ResponseWriter, r *http.Request) { 84 | ctx := appengine.NewContext(r) 85 | 86 | if os.Getenv("BLOCK_WRITES") != "" { 87 | http.Error(w, "this is a read only instance, sorry", http.StatusForbidden) 88 | return 89 | } 90 | 91 | e, err := decodeEvent(r.Body) 92 | if err != nil { 93 | http.Error(w, err.Error(), http.StatusBadRequest) 94 | return 95 | } 96 | 97 | key := datastore.NewIncompleteKey(ctx, eventKind, nil) 98 | if _, err := datastore.Put(ctx, key, e); err != nil { 99 | http.Error(w, err.Error(), http.StatusInternalServerError) 100 | return 101 | } 102 | 103 | w.WriteHeader(http.StatusCreated) 104 | } 105 | 106 | func decodeEvent(r io.Reader) (*Event, error) { 107 | // Using an anonymous type instead of Event because the date format 108 | // we need to parse is not standard. 109 | // You can find more on this in this talk: https://talks.golang.org/2015/json.slide 110 | var data struct { 111 | Title string 112 | Date string 113 | Location string 114 | Description string 115 | } 116 | 117 | err := json.NewDecoder(r).Decode(&data) 118 | if err != nil { 119 | return nil, fmt.Errorf("could not decode JSON: %v", err) 120 | } 121 | 122 | if data.Title == "" { 123 | return nil, fmt.Errorf("title is required") 124 | } 125 | if data.Location == "" { 126 | return nil, fmt.Errorf("location is required") 127 | } 128 | t, err := time.Parse("2006-01-02", data.Date) 129 | if err != nil { 130 | return nil, fmt.Errorf("could not parse date: %v", err) 131 | } 132 | 133 | return &Event{ 134 | Title: data.Title, 135 | Date: t, 136 | Description: data.Description, 137 | Location: data.Location, 138 | }, nil 139 | } 140 | -------------------------------------------------------------------------------- /section05/README.md: -------------------------------------------------------------------------------- 1 | # 5: Hello, HTML 2 | 3 | In this chapter you will learn how to serve HTML pages on App Engine and how to 4 | make the HTML and JavaScript components communicate with your Go code. 5 | 6 | ## Only static content 7 | 8 | For now let's start with a simple HTML page: 9 | 10 | [embedmd]:# (all_static/hello.html /.*DOCTYPE/ $) 11 | ```html 12 | 13 | 14 | 15 | 16 | Hello, App Engine 17 | 18 | 19 |

Hello, App Engine

20 | 21 | 22 | ``` 23 | 24 | We can create a new `app.yaml` to serve this static page: 25 | 26 | [embedmd]:# (all_static/app.yaml) 27 | ```yaml 28 | runtime: go 29 | api_version: go1 30 | 31 | handlers: 32 | - url: /hello 33 | static_files: hello.html 34 | upload: hello.html 35 | ``` 36 | 37 | As you can see we are handling the requests with path `/hello` displaying 38 | the contents of the `hello.html` file. 39 | 40 | Try running your application locally (you can find all the files 41 | [here](./all_static)). This should fail, because the Go runtime requires 42 | having some Go code in it! 43 | 44 | There's two solutions for this: 45 | 46 | - use the Python runtime which doesn't require any Python code 47 | 48 | - or add some Go code, something as simple as this `dummy.go` file: 49 | 50 | [embedmd]:# (all_static/dummy.go /package dummy/ $) 51 | ```go 52 | package dummy 53 | ``` 54 | 55 | We will do the latter as we'll add more Go code later on. 56 | 57 | Try running your application again: 58 | 59 | ```bash 60 | $ dev_appserver.py . 61 | ``` 62 | 63 | Or deploying it: 64 | 65 | ```bash 66 | $ gcloud app deploy app.yaml 67 | ``` 68 | 69 | And verify that the output matches your expectations: 70 | 71 | ![Hello, App Engine](screenshot.png) 72 | 73 | ## Serving dynamic content: HTML + Go 74 | 75 | Static content is often not enough and we need our web app frontend (HTML + JS) 76 | to communicate with our backend. 77 | 78 | To do so we have two different options: 79 | 80 | - use `app.yaml` to determine what requests are handled by which part, or 81 | - use multiple modules on possibly different runtimes. 82 | 83 | You can learn more about Go modules on this 84 | [documentation](https://cloud.google.com/appengine/docs/go/modules/). 85 | 86 | For this workshop we will go with the simple option and just enhance the 87 | previous `app.yaml` to match our requirements. 88 | 89 | We will need three components: 90 | 91 | - a Go program similar to the previous one, 92 | - an HTML page with pure static content, and 93 | - an `app.yaml` file to glue everything together. 94 | 95 | The only part that changes here is the `app.yaml`: 96 | 97 | [embedmd]:# (mixed_content/app.yaml) 98 | ```yaml 99 | runtime: go 100 | api_version: go1 101 | 102 | handlers: 103 | # requests with empty paths are shown the html page. 104 | - url: / 105 | static_files: hello.html 106 | upload: hello.html 107 | # requests with the /api/ path are handled by the Go app. 108 | - url: /api/.* 109 | script: _go_app 110 | ``` 111 | 112 | Try changing the `/api/hello` url on the second handler to `/api/backend`. 113 | Why does it fail? Fix it. 114 | 115 | ### Accessing the backend from the frontend 116 | 117 | This can be done in *many* ways, as many as JavaScript frameworks you can think 118 | of. For this simple example we will simply use [jQuery](https://jquery.com/). 119 | 120 | 121 | [embedmd]:# (mixed_content/hello.html /.*DOCTYPE/ $) 122 | ```html 123 | 124 | 125 | 126 | 127 | Hello, App Engine 128 | 129 | 130 | 131 |

Hello, App Engine

132 |

The backend says:

133 | 138 | 139 | 140 | ``` 141 | 142 | ## Directory organization for bigger applications 143 | 144 | As you can imagine very soon you will want to serve many static files, some 145 | HTML, JavaScript, CSS, etc. Rather than listing each of those on your `app.yaml` 146 | there's a much simpler option once you put all of them in a single directory. 147 | 148 | my_app 149 | |- app.yaml 150 | |- hello.go 151 | \- static 152 | |- index.html 153 | |- style.css 154 | \- script.js 155 | 156 | Your `app.yaml` in this case will look like this: 157 | 158 | [embedmd]:# (static_dirs/app.yaml /runtime/ $) 159 | ```yaml 160 | runtime: go 161 | api_version: go1 162 | 163 | handlers: 164 | # requests starting with /api/ are handled by the Go app. 165 | - url: /api/.* 166 | script: _go_app 167 | 168 | # if the path is empty show index.html. 169 | - url: / 170 | static_files: static/index.html 171 | upload: static/index.html 172 | 173 | # otherwise try to find it in the static directory. 174 | - url: / 175 | static_dir: static 176 | ``` 177 | 178 | This kind of structure provides a clear structure of the application. 179 | 180 | ## Exercise: let's build a whole app! 181 | 182 | OK, we know now enough stuff to build an application. So, let's do it! 🎉 183 | 184 | Have a look at this [introduction](../events) describing the application 185 | and implement *ONLY* [step 0](../events/step0/README.md). Then come back for more. 186 | 187 | # Congratulations! 188 | 189 | You just created your first application where a web frontend communicates with 190 | your Go backend! 191 | 192 | But, shouldn't the backend generate JSON rather than plain text? 193 | Let's learn about JSON encoding and decoding on the [next section](../section06/README.md). 194 | -------------------------------------------------------------------------------- /section01/README.md: -------------------------------------------------------------------------------- 1 | # 1: Web Clients 2 | 3 | In this section you're going to learn everything you need to know to write a web servers and clients in Go. 4 | 5 | The `net/http` package provides a series of functions and types to help you sending HTTP requests. 6 | The most important types are: 7 | 8 | - the [Client](https://golang.org/pkg/net/http#Client) 9 | - the [Request](https://golang.org/pkg/net/http#Request) 10 | - the [Response](https://golang.org/pkg/net/http#Response) 11 | 12 | We'll get to see how those types work in a minute. 13 | But before that it is important to realize that there's helper functions that will make our life easier here too. 14 | 15 | ## The Get method 16 | 17 | One of those helper functions is [`Get`](https://golang.org/pkg/net/http#Get). 18 | 19 | [embedmd]:# (examples/get.go /package main/ $) 20 | ```go 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "log" 26 | "net/http" 27 | ) 28 | 29 | func main() { 30 | // try changing the value of this url 31 | res, err := http.Get("https://golang.org") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | fmt.Println(res.Status) 36 | } 37 | ``` 38 | [source code](examples/get.go) 39 | 40 | This program will send a GET HTTP request to the Go homepage and will print the status code of the response, 41 | unless something goes wrong in which case it will log the error and stop the execution of the program. 42 | 43 | Try changing the value of the URL to see what other codes you're able to get. 44 | Some ideas you could try: 45 | 46 | - https://golang.org/foo 47 | - https://thisurldoesntexist.com 48 | - https:/thisisnotaurl 49 | 50 | ### Exercise status codes 51 | 52 | The `Get` function we just used returns a `Response` and an `error`. 53 | Read the documentation of `Response` and modify `get.go` so it prints a message only when the status code of the response is 404. 54 | 55 | ### Exercise body 56 | 57 | The `Response` type also has a `Body` field of type `io.ReadCloser`. 58 | The type of this field is a big hint on what we can do with it: read it and close it. 59 | 60 | Modify the program `get.go` so it will also print the body of the `Response` onto the standard output. 61 | Remember that the `Body` should be closed at the end of your program to avoid leaking memory! 62 | 63 | ## Other methods 64 | 65 | The `Get` method we just used is a helper function that calls the `Get` method of the `http.DefaultClient`. 66 | The `Client` type offers some other methods related directly to the HTTP methods we all know: 67 | 68 | - [Client.Get](https://golang.org/pkg/net/http#Client.Get) 69 | - [Client.Post](https://golang.org/pkg/net/http#Client.Post) 70 | - [Client.PostForm](https://golang.org/pkg/net/http#Client.PostForm) 71 | - [Client.Head](https://golang.org/pkg/net/http#Client.Head) 72 | 73 | and the equivalent helper functions: 74 | 75 | - [http.Get](https://golang.org/pkg/net/http#Get) 76 | - [http.Post](https://golang.org/pkg/net/http#Post) 77 | - [http.PostForm](https://golang.org/pkg/net/http#PostForm) 78 | - [http.Head](https://golang.org/pkg/net/http#Head) 79 | 80 | What if you want to use some other methods? Meet the [`Do`](https://golang.org/pkg/net/http#Client.Do) method. 81 | This method receives a pointer to `http.Request` as parameters and return a `http.Response` and an `error`. 82 | 83 | The `Request` method provides all the expressivity we need to send any kind of HTTP requests. 84 | 85 | For instance, we can create the equivalent request to the original `get.go` program: 86 | 87 | The `Client` type exposes the `Do` method that send the given `Request` and returns a `Response` and an `error`. 88 | 89 | [embedmd]:# (examples/do-get.go /package main/ $) 90 | ```go 91 | package main 92 | 93 | import ( 94 | "fmt" 95 | "log" 96 | "net/http" 97 | ) 98 | 99 | func doGet() { 100 | req, err := http.NewRequest("GET", "https://golang.org", nil) 101 | if err != nil { 102 | log.Fatalf("could not create request: %v", err) 103 | } 104 | client := http.DefaultClient 105 | res, err := client.Do(req) 106 | if err != nil { 107 | log.Fatalf("http request failed: %v", err) 108 | } 109 | fmt.Println(res.Status) 110 | } 111 | ``` 112 | [source code](examples/do-get.go) 113 | 114 | This also allows us to provide a `Body` for the `Request`, similar to the one we had with the `Response`. 115 | The big question is how to create an `io.Reader`? 116 | 117 | Well, it depends on the type of what you want to read, but if you want to provide a `string` it is quite easy. 118 | You can create an `io.Reader` from a `string` with [`strings.NewReader`](https://golang.org/pkg/strings#NewReader). 119 | 120 | ### Exercise PUT request 121 | 122 | Modify the code above to send a PUT request to https://http-methods.appspot.com/YourName/Message. 123 | Replace `YourName` with your name, or something unique that no one else might be using. 124 | This will save whatever string you send in the Body of the request so you can retrieve it later with: 125 | 126 | ``` 127 | $ curl https://http-methods.appspot.com/YourName/Message 128 | ``` 129 | 130 | ## Parameters: queries and forms 131 | 132 | The web application behind https://http-methods.appspot.com supports printing all the keys under a namespace. 133 | Try visiting https://http-methods.appspot.com/Hungary/ and you'll see all the keys in that namespace. 134 | To also show the values you can add `?v=true` to the url: https://http-methods.appspot.com/Hungary/?v=true . 135 | 136 | ### Exercise queries 137 | 138 | Write a program that will fetch and display all the keys and values in a namespace. 139 | Rather than adding the `?v=true` at the end of the URL passed to `NewRequest` find the way to add it 140 | directly in the `Request` that you pass to `Do`. 141 | 142 | # Congratulations! 143 | 144 | Good job! You now know pretty much everything there is to know (today) for HTTP clients, requests, and responses. 145 | Now, to understand how all of this is handled on the server side, let's go the [next section](../section02/README.md). 146 | -------------------------------------------------------------------------------- /utils/http-methods/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to writing, software distributed 8 | // under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR 9 | // CONDITIONS OF ANY KIND, either express or implied. 10 | // 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package app 15 | 16 | import ( 17 | "fmt" 18 | "io/ioutil" 19 | "net/http" 20 | 21 | "google.golang.org/appengine" 22 | "google.golang.org/appengine/datastore" 23 | "google.golang.org/appengine/log" 24 | 25 | "github.com/gorilla/mux" 26 | ) 27 | 28 | const ( 29 | namespaceKind = "namespace" 30 | valueKind = "value" 31 | ) 32 | 33 | type value struct{ Value string } 34 | 35 | func init() { 36 | r := mux.NewRouter() 37 | r.Handle("/", csrfHandler{http.HandlerFunc(getNamespaces)}).Methods("GET") 38 | r.Handle("/{namespace}/", csrfHandler{withNamespace{getAll, false}}).Methods("GET") 39 | r.Handle("/{namespace}/{key}", csrfHandler{withNamespace{getOne, false}}).Methods("GET") 40 | r.Handle("/{namespace}/{key}", csrfHandler{withNamespace{put, true}}).Methods("PUT") 41 | r.Handle("/{namespace}/{key}", csrfHandler{withNamespace{delete, false}}).Methods("DELETE") 42 | http.Handle("/", r) 43 | } 44 | 45 | func getNamespaces(w http.ResponseWriter, r *http.Request) { 46 | ctx := appengine.NewContext(r) 47 | log.Infof(ctx, "getNamespaces") 48 | 49 | keys, err := datastore.NewQuery(namespaceKind).KeysOnly().GetAll(ctx, nil) 50 | if err != nil { 51 | http.Error(w, err.Error(), http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | w.Header().Add("Content-Type", "text/plain") 56 | for _, key := range keys { 57 | fmt.Fprintln(w, key.StringID()) 58 | } 59 | } 60 | 61 | type csrfHandler struct{ h http.Handler } 62 | 63 | func (h csrfHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 64 | w.Header().Set("Access-Control-Allow-Origin", "*") 65 | h.h.ServeHTTP(w, r) 66 | } 67 | 68 | type withNamespace struct { 69 | h func(w http.ResponseWriter, r *http.Request, namespace *datastore.Key) 70 | 71 | createIfMissing bool 72 | } 73 | 74 | func (h withNamespace) ServeHTTP(w http.ResponseWriter, r *http.Request) { 75 | ctx := appengine.NewContext(r) 76 | 77 | namespace := mux.Vars(r)["namespace"] 78 | key := datastore.NewKey(ctx, namespaceKind, namespace, 0, nil) 79 | if err := datastore.Get(ctx, key, new(struct{})); err == datastore.ErrNoSuchEntity { 80 | if !h.createIfMissing { 81 | http.Error(w, fmt.Sprintf("namespace %s not found", namespace), http.StatusNotFound) 82 | return 83 | } 84 | 85 | _, err := datastore.Put(ctx, key, new(struct{})) 86 | if err != nil { 87 | http.Error(w, fmt.Sprintf("could not create namespace %s: %v", namespace, err), http.StatusInternalServerError) 88 | return 89 | } 90 | } else if err != nil { 91 | http.Error(w, fmt.Sprintf("fetching namespace %s: %v", namespace, err), http.StatusInternalServerError) 92 | return 93 | } 94 | h.h(w, r, key) 95 | } 96 | 97 | func getAll(w http.ResponseWriter, r *http.Request, namespace *datastore.Key) { 98 | ctx := appengine.NewContext(r) 99 | log.Infof(ctx, "getAll") 100 | 101 | values := []value{} 102 | keys, err := datastore.NewQuery(valueKind).Ancestor(namespace).GetAll(ctx, &values) 103 | if err != nil { 104 | http.Error(w, err.Error(), http.StatusInternalServerError) 105 | return 106 | } 107 | 108 | w.Header().Add("Content-Type", "text/plain") 109 | printValues := r.FormValue("v") == "true" 110 | if printValues { 111 | for i, key := range keys { 112 | fmt.Fprintf(w, "%s:%s\n", key.StringID(), values[i].Value) 113 | } 114 | } else { 115 | for _, key := range keys { 116 | fmt.Fprintln(w, key.StringID()) 117 | } 118 | } 119 | } 120 | 121 | func getOne(w http.ResponseWriter, r *http.Request, namespace *datastore.Key) { 122 | ctx := appengine.NewContext(r) 123 | log.Infof(ctx, "getOne") 124 | 125 | vars := mux.Vars(r) 126 | keyName := vars["key"] 127 | key := datastore.NewKey(ctx, valueKind, keyName, 0, namespace) 128 | 129 | var v value 130 | if err := datastore.Get(ctx, key, &v); err == datastore.ErrNoSuchEntity { 131 | http.Error(w, fmt.Sprintf("key %s not found in namespace %v", keyName, namespace.StringID()), http.StatusNotFound) 132 | return 133 | } else if err != nil { 134 | http.Error(w, fmt.Sprintf("fetching value %s in %s: %v", keyName, namespace.StringID(), err), http.StatusInternalServerError) 135 | return 136 | } 137 | 138 | w.Header().Add("Content-Type", "text/plain") 139 | fmt.Fprintln(w, v.Value) 140 | } 141 | 142 | func put(w http.ResponseWriter, r *http.Request, namespace *datastore.Key) { 143 | ctx := appengine.NewContext(r) 144 | log.Infof(ctx, "put") 145 | 146 | vars := mux.Vars(r) 147 | keyName := vars["key"] 148 | key := datastore.NewKey(ctx, valueKind, keyName, 0, namespace) 149 | 150 | b, err := ioutil.ReadAll(r.Body) 151 | if err != nil { 152 | http.Error(w, err.Error(), http.StatusInternalServerError) 153 | return 154 | } 155 | v := value{string(b)} 156 | 157 | if _, err := datastore.Put(ctx, key, &v); err != nil { 158 | http.Error(w, err.Error(), http.StatusInternalServerError) 159 | } 160 | } 161 | 162 | func delete(w http.ResponseWriter, r *http.Request, namespace *datastore.Key) { 163 | ctx := appengine.NewContext(r) 164 | log.Infof(ctx, "delete") 165 | 166 | vars := mux.Vars(r) 167 | keyName := vars["key"] 168 | 169 | key := datastore.NewKey(ctx, valueKind, keyName, 0, namespace) 170 | if err := datastore.Delete(ctx, key); err != nil { 171 | http.Error(w, fmt.Sprintf("fetching value %s in %s: %v", keyName, namespace, err), http.StatusInternalServerError) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /section06/README.md: -------------------------------------------------------------------------------- 1 | # 6: JSON encoding and decoding 2 | 3 | Go's standard library provides JSON encoding and decoding with the package 4 | [`encoding/json`](https://golang.org/pkg/encoding/json). 5 | 6 | ## Encoding and decoding JSON 7 | 8 | Let's first learn how to encode and decode JSON in a general way and we'll 9 | see afterwards how to do it inside of an HTTP server. 10 | 11 | ### JSON and Go structs 12 | 13 | The easiest way to encode and decode JSON objects with Go is to create a Go type 14 | which matches the structure of the JSON object we want to decode. 15 | 16 | So given a JSON object like this: 17 | 18 | ```json 19 | { 20 | "name": "gopher", 21 | "age_years": 5 22 | } 23 | ``` 24 | 25 | We would create a type containing the same fields: 26 | 27 | ```go 28 | type Person struct { 29 | Name string 30 | AgeYears int 31 | } 32 | ``` 33 | 34 | Note that the all the identifiers (both type and fields) start with an uppercase 35 | letter. This is because only identifiers starting with an uppercase are exported 36 | outside of a package. So if the field `Name` was `name` the `encoding/json` 37 | package wouldn't be able to even know it is there. 38 | 39 | Fortunately we can use field tags to modify what name is used in the JSON form 40 | for each Go field. 41 | 42 | For instance we would add the following field tags to the previous example: 43 | 44 | [embedmd]:# (examples/app.go /type Person/ /^}/) 45 | ```go 46 | type Person struct { 47 | Name string `json:"name"` 48 | AgeYears int `json:"age_years"` 49 | } 50 | ``` 51 | 52 | _Note_: the backticks ```(`)``` are just a different way to write strings in Go. 53 | They allow you to use double quotes `(")` and to expand across multiple lines. 54 | 55 | For more info on structs read 56 | [this section](https://tour.golang.org/moretypes/5) of the Go tour. 57 | 58 | ### Encoding Go structs to JSON 59 | 60 | To encode a Go struct we use a 61 | [`json.Encoder`](https://golang.org/pkg/encoding/json#Encoder), which provides 62 | a handy `Encode` method. 63 | 64 | [embedmd]:# (examples/app.go /func encode/ /^}/) 65 | ```go 66 | func encode() { 67 | p := Person{"gopher", 5} 68 | 69 | // create an encoder that will write on the standard output. 70 | enc := json.NewEncoder(os.Stdout) 71 | // use the encoder to encode p, which could fail. 72 | err := enc.Encode(p) 73 | // if it failed, log the error and stop execution. 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | } 78 | ``` 79 | 80 | This code snippet shows how to handle errors every time we encode a value, 81 | and while in the example it seems impossible to have an error consider that 82 | the encoder output could be sent through a network connection. 83 | 84 | You can try the code with the `go run` tool, or using the Go playground 85 | [here](https://play.golang.org/p/rsO0Vk-9Xl). 86 | 87 | ### Decoding JSON objects into Go structs 88 | 89 | The same way we have a `json.Encoder` we have a `json.Decoder` and its usage 90 | is very similar. 91 | 92 | [embedmd]:# (examples/app.go /func decode/ /^}/) 93 | ```go 94 | func decode() { 95 | // create an empty Person value. 96 | var p Person 97 | 98 | // create a decoder reading from the standard input. 99 | dec := json.NewDecoder(os.Stdin) 100 | // use the decoder to decode a value into p. 101 | err := dec.Decode(&p) 102 | // if it failed, log the error and stop execution. 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | // otherwise log what we decoded. 107 | fmt.Printf("decoded: %#v\n", p) 108 | } 109 | ``` 110 | 111 | Note that the parameter for `dec.Decode` is not `p` but `&p`. This is a 112 | pointer to the variable `p` so the `encoding/json` package can modify the 113 | value of `p`. Otherwise we would pass a copy of `p` and any modifications 114 | would be without side effects. 115 | 116 | Read more about pointers in the [Go tour](https://tour.golang.org/moretypes/1). 117 | 118 | ## encoding/json + net/http = web services! 119 | 120 | Let's have another look at the `http.HandlerFunc` type: 121 | 122 | ```go 123 | type HandlerFunc func(ResponseWriter, *Request) 124 | ``` 125 | 126 | ### Encoding JSON onto a http.ResponseWriter 127 | 128 | As we mentioned before `http.ResponseWriter` implements the method `Write` and 129 | therefore satisfies the `io.Writer` interface required by `json.NewEncoder`. 130 | 131 | So we can easily JSON encode a `Person` on an HTTP response: 132 | 133 | [embedmd]:# (examples/app.go /func encodeHandler/ /^}/) 134 | ```go 135 | func encodeHandler(w http.ResponseWriter, r *http.Request) { 136 | p := Person{"gopher", 5} 137 | 138 | // set the Content-Type header. 139 | w.Header().Set("Content-Type", "application/json") 140 | 141 | // encode p to the output. 142 | enc := json.NewEncoder(w) 143 | err := enc.Encode(p) 144 | if err != nil { 145 | // if encoding fails, create an error page with code 500. 146 | http.Error(w, err.Error(), http.StatusInternalServerError) 147 | } 148 | } 149 | ``` 150 | 151 | ### Decoding JSON from a http.Request 152 | 153 | The `http.Request` type is a struct and it has a field named `Body` of type 154 | `io.ReadCloser`, an interface with the methods `Read` and `Close`. 155 | 156 | Since the signature of the method `Read` matches the one in `io.Reader` we can 157 | say that `io.ReadCloser` is an `io.Reader` and therefore we can use the `Body` 158 | of a `http.Request` as the input of a `json.Decoder`. 159 | 160 | [embedmd]:# (examples/app.go /func decodeHandler/ /^}/) 161 | ```go 162 | func decodeHandler(w http.ResponseWriter, r *http.Request) { 163 | var p Person 164 | 165 | dec := json.NewDecoder(r.Body) 166 | err := dec.Decode(&p) 167 | if err != nil { 168 | http.Error(w, err.Error(), http.StatusBadRequest) 169 | return 170 | } 171 | fmt.Fprintf(w, "Name is %v and age is %v", p.Name, p.AgeYears) 172 | } 173 | ``` 174 | 175 | If you want to test this handler you can use curl: 176 | 177 | ```bash 178 | $ curl -d '{"name": "gopher", "age_years": 5}' http://localhost:8080/ 179 | Name is gopher and age is 5 180 | ``` 181 | 182 | ## Exercise 183 | 184 | Add JSON encoding and decoding to the events application with [step 1](../events/step1/README.md). 185 | Then come back here for more! 186 | 187 | # Congratulations! 188 | 189 | You've successfully built a web application where the backend and the frontend 190 | interact via JSON messages over HTTP requests: that's pretty much as RESTful as 191 | it gets! 192 | 193 | But what if we want to store some of that information we're decoding? 194 | 195 | Continue to the [next chapter](../section07/README.md) to learn about 196 | the Google Cloud Datastore. 197 | -------------------------------------------------------------------------------- /section09/README.md: -------------------------------------------------------------------------------- 1 | # 9: What is Memcache and how to use it from App Engine 2 | 3 | Memcache is one of the most well known caching system, developed originally by 4 | [Brad Fitzpatrick](https://twitter.com/bradfitz), a current member of the Go team! 5 | 6 | App Engine provides a way an incredibly simple way to use Memcache, similarly to 7 | the way you can use Datastore. You get access to completely managed Memcache 8 | instances that are maintained, scaled, and updated by Google so you don't need to 9 | care about anything other than your business. 10 | 11 | Memcache will keep your data for a period of time you can specify and will drop 12 | the values once a given time has passed. You should not store anything that 13 | you can't recover in Memcache, as there's no durability warranty. In other 14 | words, if Memcache was not working your application should still work perfectly, 15 | even though it would probably be a bit slower. 16 | 17 | ## Using it from the Go App Engine runtime 18 | 19 | As for the packages seen before there's a package provided by the Go runtime 20 | named [`google.golang.org/appengine/memcache`](https://godoc.org/google.golang.org/appengine/memcache). 21 | 22 | There's two main operations you can do with memcache: 23 | 24 | - caching data with `memcache.Set`, and 25 | - retrieve data back with `memcache.Get`. 26 | 27 | ### Caching data 28 | 29 | We will use the `memcache.Set` function: 30 | 31 | ```go 32 | func Set(c appengine.Context, item *Item) error 33 | ``` 34 | 35 | The first parameter is an `appengine.Context`, as usual, and the second one is 36 | the item we want to store. Let's concentrate on three fields of the 37 | `memcache.Item` type for now: 38 | 39 | ```go 40 | type Item struct { 41 | Key string 42 | Value []byte 43 | Expiration time.Duration 44 | 45 | // I removed some fields for clarity here 46 | } 47 | ``` 48 | 49 | - Keys, which are simple strings, let you identify values. 50 | - Values are a slice of bytes of up to 1Mb in size. 51 | - The Expiration fields indicates for how long the value is valid. 52 | 53 | So if we want to store an item with key "name" and value "gopher" that will be 54 | valid for one hour we could write: 55 | 56 | [embedmd]:# (getset/app.go /package app/ /^}/) 57 | ```go 58 | package app 59 | 60 | import ( 61 | "fmt" 62 | "net/http" 63 | "time" 64 | 65 | "google.golang.org/appengine" 66 | "google.golang.org/appengine/memcache" 67 | ) 68 | 69 | func set(w http.ResponseWriter, r *http.Request) { 70 | ctx := appengine.NewContext(r) 71 | 72 | // get the parameters k and v from the request 73 | key := r.FormValue("k") 74 | value := r.FormValue("v") 75 | 76 | item := &memcache.Item{ 77 | Key: key, 78 | Value: []byte(value), 79 | Expiration: 1 * time.Hour, 80 | } 81 | 82 | err := memcache.Set(ctx, item) 83 | if err != nil { 84 | http.Error(w, err.Error(), http.StatusInternalServerError) 85 | } 86 | } 87 | ``` 88 | 89 | ### Retrieving cache data 90 | 91 | To retrieve an item from Memcache we use the `memcache.Get` function: 92 | 93 | ```go 94 | func Get(c appengine.Context, key string) (*Item, error) 95 | ``` 96 | 97 | Given a key `memcache.Get` returns the corresponding item if any, containing the 98 | store value in the `Value` field. If the key was not found in the cache the 99 | returned error is `memcache.ErrCacheMiss`. 100 | 101 | Let's retrieve the value we cached on the previous code: 102 | 103 | [embedmd]:# (getset/app.go /func get/ /^}/) 104 | ```go 105 | func get(w http.ResponseWriter, r *http.Request) { 106 | ctx := appengine.NewContext(r) 107 | 108 | key := r.FormValue("k") 109 | 110 | item, err := memcache.Get(ctx, key) 111 | switch err { 112 | case nil: 113 | fmt.Fprintf(w, "%s", item.Value) 114 | case memcache.ErrCacheMiss: 115 | fmt.Fprint(w, "key not found") 116 | default: 117 | http.Error(w, err.Error(), http.StatusInternalServerError) 118 | } 119 | } 120 | ``` 121 | 122 | You can see a complete example of an application using Memcache [here](getset). 123 | 124 | Test it with `curl` or [Postman](https://chrome.google.com/webstore/detail/fdmmgilgnpjigdojojpjoooidkmcomcm?utm_source=chrome-ntp-launcher). 125 | 126 | ## Using Codecs 127 | 128 | As you can imagine using Memcache to store structure data this way can be a 129 | little bit of a hassle, that's why the package also offers a way to use 130 | coders and encoders, aka codecs, such as JSON. 131 | 132 | When using codecs rather than setting the `Value` field of the `memcache.Item` 133 | you should use the `Object` field instead. For instance you can cache and 134 | retrieve a `Person` using the JSON codec like this: 135 | 136 | [embedmd]:# (codec/app.go /func set/ /^}/) 137 | ```go 138 | func set(w http.ResponseWriter, r *http.Request) { 139 | ctx := appengine.NewContext(r) 140 | 141 | var p Person 142 | if err := json.NewDecoder(r.Body).Decode(&p); err != nil { 143 | http.Error(w, err.Error(), http.StatusBadRequest) 144 | return 145 | } 146 | 147 | item := &memcache.Item{ 148 | Key: "last_person", 149 | Object: p, // we set the Object field instead of Value 150 | Expiration: 1 * time.Hour, 151 | } 152 | 153 | // we use the JSON codec 154 | err := memcache.JSON.Set(ctx, item) 155 | if err != nil { 156 | http.Error(w, err.Error(), http.StatusInternalServerError) 157 | } 158 | } 159 | ``` 160 | 161 | And to retrieve it it's even simpler: 162 | 163 | [embedmd]:# (codec/app.go /func get/ /^}/) 164 | ```go 165 | func get(w http.ResponseWriter, r *http.Request) { 166 | ctx := appengine.NewContext(r) 167 | 168 | var p Person 169 | _, err := memcache.JSON.Get(ctx, "last_person", &p) 170 | if err == nil { 171 | json.NewEncoder(w).Encode(p) 172 | return 173 | } 174 | if err == memcache.ErrCacheMiss { 175 | fmt.Fprint(w, "key not found") 176 | return 177 | } 178 | http.Error(w, err.Error(), http.StatusInternalServerError) 179 | } 180 | ``` 181 | 182 | You can see a complete example of an application using the JSON code 183 | [here](codec). 184 | 185 | ## The Memcache viewer page 186 | 187 | Similarly to the Datastore admin page, you can check some statistics and find 188 | what keys are stored or used in a Memcache instance accessing the Memcache 189 | viewer page. 190 | 191 | If you're running your application locally you can access 192 | http://localhost:8000/memcache: 193 | 194 | 195 | 196 | If you want to view the statistics on a production instance you can use the 197 | Developers Console 198 | [here](https://console.developers.google.com/project/_/appengine/memcache): 199 | 200 | 201 | 202 | # Exercise: make the events app faster with memcache 203 | 204 | Remember the events app? Remember how it spent most of its time fetching weather 205 | information from the openweathermap.org API? Let's fix that with 206 | [step 4](../events/step4/README.md). 207 | 208 | # Congratulations! 209 | 210 | You're now not only able to cache and retrieve data in Memcache, but also to 211 | do so with structured data thanks to the JSON codec! 212 | 213 | Go to the [next section](../section10/README.md) for a couple of surprises! 214 | -------------------------------------------------------------------------------- /section04/README.md: -------------------------------------------------------------------------------- 1 | # 4: Deploying to App Engine 2 | 3 | In the previous chapter we learned how to write a simple HTTP server in Go as a 4 | static binary that you can execute easily on any server with the same platform. 5 | 6 | ## Getting ready for success 7 | 8 | What would happen if your "Hello, web" went viral? Would your simple binary 9 | handle the load? You would probably need to add some extra servers, which is 10 | probably a good idea just to have redundancy. 11 | 12 | How do you that? Many solutions, but the simplest one is Google App Engine. 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | With Google App Engine you provide your code and Google is responsible for 23 | handling any amount of traffic you may get, making sure all servers are up 24 | so you don't need to worry about them. Check the 25 | [docs](https://cloud.google.com/appengine/docs) out for more info, this is a 26 | workshop after all, not a marketing document. 27 | 28 | ## From Hello, web to Hello, App Engine 29 | 30 | Moving from a stand alone server to an App Engine app is quite simple, let's 31 | go through the process. 32 | 33 | ### Adapt the Go code 34 | 35 | When you deploy some code to the Google App Engine servers you don't provide a 36 | binary but just some code that will be compiled and linked on Google servers. 37 | 38 | This means you don't define either the `main` package or the `main` function. 39 | Also you don't have to choose what port you listen to as App Engine will also 40 | manage that for you. 41 | 42 | This simplifies the code we had before: 43 | 44 | [embedmd]:# (examples/hello.go /package hello/ $) 45 | ```go 46 | package hello 47 | 48 | import ( 49 | "fmt" 50 | "net/http" 51 | ) 52 | 53 | func helloHandler(w http.ResponseWriter, r *http.Request) { 54 | fmt.Fprintln(w, "Hello, App Engine") 55 | } 56 | 57 | func init() { 58 | http.HandleFunc("/hello", helloHandler) 59 | } 60 | ``` 61 | 62 | As you can see we still need to register our handler, and since that needs to 63 | happen before the App Engine app starts listening for request we do it in an 64 | `init` functions. All `init` functions in a Go program are executed after all 65 | variable declarations and before the `main` function starts. 66 | 67 | ### No more http.DefaultClient 68 | 69 | Any operation in App Engine that involves getting out of the machine are controlled by 70 | a context. This context handles security, quotas, and many other important things. 71 | 72 | This means that simply running `http.Get` to fetch some remote page will fail. That's sad. 73 | How do we fix it? Meet the `urlfetch` package! 74 | 75 | The `urlfetch` package is defined in [google.golang.org/appengine/urlfetch](https://google.golang.org/appengine/urlfetch), 76 | and to obtain a new HTTP client we call the `Client` function that requires an `appengine.Context`. 77 | 78 | Before you start using it you need to download it to your machine. Simply run: 79 | 80 | ```bash 81 | $ go get -u google.golang.org/appengine/... 82 | ``` 83 | 84 | To create a new `appengine.Context` we need to call `appengine.NewContext` and pass an HTTP request. 85 | 86 | [embedmd]:# (app/app.go /package app/ $) 87 | ```go 88 | package app 89 | 90 | import ( 91 | "fmt" 92 | "net/http" 93 | 94 | "google.golang.org/appengine" 95 | "google.golang.org/appengine/urlfetch" 96 | ) 97 | 98 | func init() { 99 | http.HandleFunc("/", handler) 100 | } 101 | 102 | func handler(w http.ResponseWriter, r *http.Request) { 103 | // first create a new context 104 | c := appengine.NewContext(r) 105 | // and use that context to create a new http client 106 | client := urlfetch.Client(c) 107 | 108 | // now we can use that http client as before 109 | res, err := client.Get("http://google.com") 110 | if err != nil { 111 | http.Error(w, fmt.Sprintf("could not get google: %v", err), http.StatusInternalServerError) 112 | return 113 | } 114 | fmt.Fprintf(w, "Got Google with status %s\n", res.Status) 115 | } 116 | ``` 117 | 118 | We will see how the `appengine.Context` is used for basically everything on App Engine. 119 | 120 | ### Write your app.yaml 121 | 122 | In addition to the code above, Google App Engine also requires a description 123 | file named `app.yaml`. This file describes the application, its runtime, and 124 | gives a list of handlers to be executed depending on the request path similarly 125 | to what we did with `HandleFunc`. 126 | 127 | This would be the `app.yaml` file for our Hello, App Engine application. 128 | 129 | [embedmd]:# (app/app.yaml) 130 | ```yaml 131 | runtime: go # the runtime (python, java, go, php) 132 | api_version: go1 # the runtime version 133 | 134 | handlers: 135 | - url: /.* # for all requests 136 | script: _go_app # pass the request to the Go code 137 | ``` 138 | 139 | ## Run the application locally 140 | 141 | Once we have the `main.go` and `app.yaml` files in a directory we can run the 142 | application locally by going to the directory and executing the `dev_appserver.py` tool 143 | that comes with the Go SDK for App Engine. 144 | 145 | ```bash 146 | $ dev_appserver.py . 147 | ``` 148 | 149 | You will see many logs, check for errors, and if everything works fine you will 150 | see a message like: 151 | 152 | ```bash 153 | INFO 2016-08-31 15:21:05,793 dispatcher.py:197] Starting module "default" running at: http://localhost:8080 154 | ``` 155 | 156 | Visit http://localhost:8080/hello and you should see your beautiful web app 157 | again. 158 | 159 | Try editing the code to change the message printed to the output. If you 160 | refresh your browser, you can see that your changes get displayed without 161 | having to restart the server. 162 | 163 | ## Deploy to the App Engine servers 164 | 165 | Once you're happy with how your application looks you might want to share it 166 | with the world, to do so you will need to create a Google Cloud Platform 167 | project. 168 | 169 | 1. Visit https://console.developers.google.com and log in with your credentials. 170 | 1. Click on `create a project` and choose a name and project ID 171 | 1. Run `gcloud init` and choose your recently created project. No need to set Compute zones. 172 | 173 | That's it! You can now deploy your code to the Google App Engine servers! 174 | 175 | Modify the `app.yaml` changing the `application` line to contain the project ID 176 | of the project you just created and deploy it running: 177 | 178 | ```bash 179 | $ gcloud app deploy app.yaml 180 | ``` 181 | 182 | Once this succeeds your app is available on https://your-project-id.appspot.com, 183 | or running: 184 | 185 | ```bash 186 | $ gcloud app browse 187 | ``` 188 | 189 | ### Exercise Deploy to App Engine 190 | 191 | Follow the instructions above and deploy the code you've been working on so far 192 | to App Engine. 193 | 194 | # Congratulations! 195 | 196 | You just deployed your first web app to Google App Engine! Maybe it's time to 197 | tell the world about it? No more localhost on your URLs! 198 | 199 | Or maybe it's a bit too early ... let's see if we can make the application look 200 | a bit better using HTML in addition to our Go code. 201 | 202 | Continue to [the next section](../section05/README.md). 203 | -------------------------------------------------------------------------------- /section02/README.md: -------------------------------------------------------------------------------- 1 | # 2: Web servers 2 | 3 | In this section you'll learn how to write a simple HTTP server in Go. 4 | 5 | We will use the [`net/http`](https://golang.org/pkg/net/http) package to do so, click 6 | on that link to browse its documentation. 7 | 8 | ### Defining HTTP handlers 9 | 10 | The `net/http` package defines the [`HandlerFunc`](https://golang.org/pkg/net/http#HandlerFunc) type: 11 | 12 | ```go 13 | type HandlerFunc func(ResponseWriter, *Request) 14 | ``` 15 | 16 | The first parameter of this function type is a 17 | [`ResponseWriter`](https://golang.org/pkg/net/http#ResponseWriter), which 18 | provides a way to set headers on the HTTP response. It also provides a `Write` 19 | method which makes it satisfy the `io.Writer` interface. 20 | 21 | Let's see a very simple HTTP handler that simply writes `"Hello, web"` to the output: 22 | 23 | [embedmd]:# (examples/step1/main.go /package main/ /^}/) 24 | ```go 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | "net/http" 30 | ) 31 | 32 | func helloHandler(w http.ResponseWriter, r *http.Request) { 33 | fmt.Fprintln(w, "Hello, web") 34 | } 35 | ``` 36 | 37 | As you can see we're using the `fmt.Fprintln` function, whose first parameter 38 | is an `io.Writer`. 39 | 40 | ### Registering HTTP handlers 41 | 42 | Once a handler is defined we need to inform the `http` package about it and 43 | specify when to run it. To do so we can use the `http.HandleFunc` function: 44 | 45 | ```go 46 | func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 47 | ``` 48 | 49 | The first parameter is a pattern which will be used to decide when to execute a 50 | handler, and the second argument is the handler itself. 51 | 52 | Patterns name fixed, rooted paths, like `"/favicon.ico"`, or rooted subtrees, 53 | like `"/images/"` (note the trailing slash). Longer patterns take precedence 54 | over shorter ones, so that if there are handlers registered for both 55 | `"/images/"` and `"/images/thumbnails/"`, the latter handler will be called for 56 | paths beginning `"/images/thumbnails/"` and the former will receive requests 57 | for any other paths in the `"/images/"` subtree. 58 | 59 | Let's see how to register our `helloHandler` defined above: 60 | 61 | [embedmd]:# (examples/step2/main.go /package main/ $) 62 | ```go 63 | package main 64 | 65 | import ( 66 | "fmt" 67 | "net/http" 68 | ) 69 | 70 | func helloHandler(w http.ResponseWriter, r *http.Request) { 71 | fmt.Fprintln(w, "Hello, web") 72 | } 73 | 74 | func main() { 75 | http.HandleFunc("/hello", helloHandler) 76 | } 77 | ``` 78 | 79 | Note that we're registering our handler as part of the `main` function. 80 | 81 | Try to run the code above: 82 | 83 | ```bash 84 | $ go run examples/step2/main.go 85 | ``` 86 | 87 | What happens? Well, we're missing the last piece of the puzzle: starting the 88 | web server! 89 | 90 | #### The Handler interface 91 | 92 | Using `http.HandleFunc` and passing a value of type `http.HandlerFunc` can be pretty constraining. 93 | There's also another function `http.Handle` that will accept any value satisfying the `http.Handler` interface. 94 | 95 | The [`http.Handler`](https://golang.org/pkg/net/http/#Handler) interface is defined in the `http` package as: 96 | 97 | ```go 98 | type Handler interface { 99 | ServeHTTP(ResponseWriter, *Request) 100 | } 101 | ``` 102 | 103 | And guess what, the type `http.HandlerFunc` satisfies `http.Handler` thanks to 104 | [`HandlerFunc.ServeHTTP`](https://golang.org/pkg/net/http/#HandlerFunc.ServeHTTP). 105 | 106 | The code of the `ServeHTTP` method for `HandlerFunc` is something of beauty. 107 | 108 | ```go 109 | func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { 110 | f(w, r) 111 | } 112 | ``` 113 | 114 | We will see how this interface is the extension points where web frameworks and toolkits add functionality. 115 | 116 | ### Listening and serving 117 | 118 | Once the handlers have been defined and registered we need to start the HTTP 119 | server to listen for requests and execute the corresponding handler. 120 | 121 | To do so we use the function 122 | [`http.ListenAndServe`](https://golang.org/pkg/net/http#ListenAndServe): 123 | 124 | ```go 125 | func ListenAndServe(addr string, handler Handler) error 126 | ``` 127 | 128 | The first parameter is the address on which we want the server to listen, 129 | we could use something like `"127.0.0.1:8080"` or `"localhost:80"`. 130 | 131 | The second parameter is an `http.Handler`, a type that allows you to define 132 | different ways of handling requests. Since we're using the default methods 133 | with `HandleFunc` we don't need to provide any value here: `nil` will do. 134 | 135 | And last but definitely not least the function returns an `error`. In Go, 136 | errors are handled by returning values rather than throwing exceptions. 137 | 138 | The type `error` is a predefined type (just like `int` or `bool`) and is an interface 139 | with only one method: 140 | 141 | ```go 142 | type error interface { 143 | Error() string 144 | } 145 | ``` 146 | 147 | By convention errors are the last value returned by methods and functions and 148 | when no error has occurred the returned value equals to `nil`. 149 | 150 | So if we want to check that our server started successfully and log an error 151 | otherwise we would modify our code to add a call to `ListenAndServe`. 152 | 153 | [embedmd]:# (examples/step3/main.go /package main/ $) 154 | ```go 155 | package main 156 | 157 | import ( 158 | "fmt" 159 | "log" 160 | "net/http" 161 | ) 162 | 163 | func helloHandler(w http.ResponseWriter, r *http.Request) { 164 | fmt.Fprintln(w, "Hello, web") 165 | } 166 | 167 | func main() { 168 | http.HandleFunc("/hello", helloHandler) 169 | err := http.ListenAndServe("127.0.0.1:8080", nil) 170 | if err != nil { 171 | log.Fatal(err) 172 | } 173 | } 174 | ``` 175 | 176 | Running this code should now start a web server listening on `127.0.0.1:8080`. 177 | 178 | Try it: 179 | 180 | $ go run main.go 181 | 182 | And then visit http://127.0.0.1:8080/hello. 183 | 184 | ### Exercise Bye, web 185 | 186 | Modify the program above by adding a second handler named `byeHandler` that prints `"Bye, web"` 187 | to the http response. 188 | 189 | ### Exercise Hello, Handler 190 | 191 | Modify the program from the previous example so you can replace the call to `http.HandleFunc` 192 | by a call to `http.Handle`. You will need to define a new type `helloHandler` and make that type 193 | satisfy the `http.Handler` interface. 194 | 195 | ### A better multiplexor 196 | 197 | Soon you will start having more complicated requirements to route your requests 198 | to your handlers such as: 199 | 200 | - route depending on the methods: `POST` and `GET` routed different handlers. 201 | - variable extraction from paths: `/product/{productID}/part/{partID}` 202 | 203 | These cases can be handled either by hand or using a toolkit that will plug 204 | correctly into the existing `net/http` package, such as the 205 | [Gorilla toolkit](http://www.gorillatoolkit.org/) and its `mux` package. 206 | 207 | [embedmd]:# (examples/gorilla.go /package main/ $) 208 | ```go 209 | package main 210 | 211 | import ( 212 | "log" 213 | "net/http" 214 | 215 | "github.com/gorilla/mux" 216 | ) 217 | 218 | func listProducts(w http.ResponseWriter, r *http.Request) { 219 | // list all products 220 | } 221 | 222 | func addProduct(w http.ResponseWriter, r *http.Request) { 223 | // add a product 224 | } 225 | 226 | func getProduct(w http.ResponseWriter, r *http.Request) { 227 | id := mux.Vars(r)["productID"] 228 | log.Printf("fetching product with ID %q", id) 229 | // get a specific product 230 | } 231 | 232 | func main() { 233 | r := mux.NewRouter() 234 | // match only GET requests on /product/ 235 | r.HandleFunc("/product/", listProducts).Methods("GET") 236 | 237 | // match only POST requests on /product/ 238 | r.HandleFunc("/product/", addProduct).Methods("POST") 239 | 240 | // match GET regardless of productID 241 | r.HandleFunc("/product/{productID}", getProduct) 242 | 243 | // handle all requests with the Gorilla router. 244 | http.Handle("/", r) 245 | if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil { 246 | log.Fatal(err) 247 | } 248 | } 249 | ``` 250 | 251 | Gorilla also provides packages for session management, cookies, etc. 252 | Have a look at the [documentation](www.gorillatoolkit.org/). 253 | 254 | #### Exercise Hello, {you} 255 | 256 | Using the `mux` package from the previous example write a new web server. 257 | This server will handle all HTTP requests sent to `/hello/name` with an HTTP page 258 | containing the text `"Hello, name"`. The `name` in this example can of course change, 259 | so if the request was `/hello/Francesc` the response should say `"Hello, Francesc"`. 260 | 261 | _Note_: to install the `mux` package in your machine you can use `go get`: 262 | 263 | ```bash 264 | $ go get github.com/gorilla/mux 265 | ``` 266 | 267 | # Congratulations! 268 | 269 | You just wrote your first HTTP server in Go! Isn't it awesome? Well, it doesn't 270 | do much yet but the best is to come. 271 | 272 | On the next chapter we'll learn how to validate whatever the input of your HTTP endpoints and 273 | how to signal different problems in the HTTP responses. 274 | 275 | Continue to [the next section](../section03/README.md). 276 | -------------------------------------------------------------------------------- /section03/README.md: -------------------------------------------------------------------------------- 1 | # 3: Input validation and status codes 2 | 3 | So far we've just assumed that all the HTTP requests our server receives are good. 4 | And if there's something that you should never do when writing web servers is trusting your input! 5 | 6 | In this chapter we'll see how to extract the information sent in different parts of the HTTP request. 7 | Once we have that information we'll see how can validate them, and how we can signal different errors. 8 | 9 | Let's start! 10 | 11 | ## Reading parameters from the URL 12 | 13 | We've seen how we can route a request to different handlers depending on the path. 14 | Now we're going to see how to extract then ones in the query part of the request, aka the data after `?`. 15 | 16 | The `http.Request` type has a method `FormValue` with the following docs: 17 | 18 | func (r *Request) FormValue(key string) string 19 | 20 | FormValue returns the first value for the named component of the query. POST and PUT body parameters take precedence over URL query string values. FormValue calls ParseMultipartForm and ParseForm if necessary and ignores any errors returned by these functions. If key is not present, FormValue returns the empty string. To access multiple values of the same key, call ParseForm and then inspect Request.Form directly. 21 | 22 | That's easy! So if we want to obtain the value of a parameter `q` in the URL `/hello?msg=world` 23 | we can write the next program. 24 | 25 | [embedmd]:# (examples/handlers/main.go /func paramHandler/ /^}/) 26 | ```go 27 | func paramHandler(w http.ResponseWriter, r *http.Request) { 28 | name := r.FormValue("name") 29 | if name == "" { 30 | name = "friend" 31 | } 32 | fmt.Fprintf(w, "Hello, %s!", name) 33 | } 34 | ``` 35 | 36 | ### Exercise Hello, parameter 37 | 38 | Write a web server that will answer to requests to `/hello?name=world` with an HTTP response with the text `Hello, world!`. 39 | If the `name` is not present it should print `Hello, friend!`. 40 | 41 | You can test it with your own browser, but let's try a couple things with `curl` too. 42 | Before running these think about what you expect to see and why. 43 | 44 | ```bash 45 | $ curl "localhost:8080/hello?name=world" 46 | 47 | $ curl "localhost:8080/hello?name=world&name=francesc" 48 | 49 | $ curl -X POST -d "name=francesc" "localhost:8080/hello" 50 | 51 | $ curl -X POST -d "name=francesc" "localhost:8080/hello?name=potato" 52 | ``` 53 | 54 | Think about how would you make your program print all the values given to `name`. 55 | 56 | ## Reading from the Request body 57 | 58 | Similarly to how we read from the `Body` in the `http.Response` a couple of chapter before we can read 59 | the body of the `http.Request`. 60 | 61 | Note that even though the type of `Body` in `http.Request` is `io.ReadCloser` the body will be automatically 62 | closed at the end of the execution of the http handler, so don't worry about it. 63 | 64 | There's many ways we can read from an `io.Reader`, but for now you can use `ioutil.ReadAll`, 65 | which returns a `[]byte` and an `error` if something goes wrong. 66 | 67 | [embedmd]:# (examples/handlers/main.go /func bodyHandler/ /^}/) 68 | ```go 69 | func bodyHandler(w http.ResponseWriter, r *http.Request) { 70 | b, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | fmt.Fprintf(w, "could not read body: %v", err) 73 | return 74 | } 75 | name := string(b) 76 | if name == "" { 77 | name = "friend" 78 | } 79 | fmt.Fprintf(w, "Hello, %s!", name) 80 | } 81 | ``` 82 | 83 | ### Exercise Hello, body 84 | 85 | Modify the previous exercise so instead of reading the `name` argument from a query or form value it will 86 | use whatever content of the `Body` is. Again, if the `Body` is empty the response should greet `friend`. 87 | 88 | If the call to `ioutil.ReadAll` returns an error write that error message to the output. 89 | 90 | You can test your exercise by using `curl`: 91 | 92 | ```bash 93 | $ curl -X POST -d "francesc" "localhost:8080/hello" 94 | ``` 95 | 96 | As an extra exercise remove any extra blank spaces surrounding the name. 97 | 98 | ## Communicating errors 99 | 100 | In the previous exercise we decided to just print the error if the call to `ioutil.ReadAll` failed. 101 | That is actually a pretty horrible idea, as you might imagine 😁. 102 | 103 | How should we do it? Well, the HTTP protocol defines a set of status codes that help us describe the nature of a response. 104 | We've actually used them before, when we checked if the response obtained using `Get` was `OK` or not. 105 | 106 | There are two ways of setting the status code with a `ResponseWriter`. 107 | 108 | ### Status codes with ResponseWriter.WriteHeader 109 | 110 | Using the `WriteHeader` method in `ResponseWriter` we can set the status code of the response. 111 | The parameter is an `int` so you could pass any number, but it is better to use the constants already 112 | defined in the `http` package. They all start with `Status` and you can find them [here](https://golang.org/pkg/net/http/#pkg-constants). 113 | 114 | By default, the status code of a response will be `StatusOK` aka `200`. 115 | 116 | #### Exercise better errors 117 | 118 | Modify your previous program so the response in case of error will have status code `500`. 119 | Instead of using the number `500` find the corresponding constant. 120 | 121 | Then make the status of the response be `400` when the body is empty. 122 | 123 | ### Status codes with http.Error 124 | 125 | In the previous exercise you've noticed that often when we set the status code of the response we also 126 | write the description of the error. That's why the `http.Error` function exists. 127 | 128 | func Error(w ResponseWriter, error string, code int) 129 | 130 | Error replies to the request with the specified error message and HTTP code. The error message should be plain text. 131 | 132 | A call to `Error` can then replace a call to `WriteHeader` followed by a some call writing to the `ResponseWriter`. 133 | 134 | #### Exercise status codes with http.Error 135 | 136 | Replace your calls to `WriteHeader` and `Fprintf` in the previous exercise with a call to `Error`. 137 | 138 | ### Response headers 139 | 140 | You might have noticed that if you send more than one line in the response, 141 | your browser shows it as one. Why is that? 142 | 143 | The answer is that the `net/http` packages guesses that the output is HTML, 144 | and therefore your browser concatenates the lines. For short outputs, it's hard to guess. 145 | You can see the content type of your response by adding `-v` to your `curl` command. 146 | 147 | ```bash 148 | $ curl -v localhost:8080 149 | < HTTP/1.1 200 OK 150 | < Date: Mon, 25 Apr 2016 16:14:46 GMT 151 | < Content-Length: 19 152 | < Content-Type: text/html; charset=utf-8 153 | ``` 154 | 155 | So, how do we stop the `net/http` package from guessing the content type? We specify it! 156 | To do so we need to set the header `"Content-Type"` to the value `"text/plain"`. 157 | You can set headers in the response with the `Header` function in the `ResponseWriter`. 158 | 159 | `Header` returns a [`http.Header`](https://golang.org/pkg/net/http/#Header) which has, among other methods, 160 | the method `Set`. We can then set the content type in our `ResponseWriter` named `w` like this. 161 | 162 | [embedmd]:# (examples/texthandler/main.go /w.Header.*/) 163 | ```go 164 | w.Header().Set("Content-Type", "text/plain") 165 | ``` 166 | 167 | ### Avoiding repetition 168 | 169 | Imagine if you had hundreds of different http handlers, and you want to set the content type header 170 | in each and every one of them. That sounds painful, doesn't it? 171 | 172 | Let me tell you about a cool technique that helps defining behavior shared by many handlers. 173 | Some people call them decorators, most of them also write Python 😛. 174 | 175 | To start we're going to define a new type named `textHandler` that contains a `http.HandlerFunc`. 176 | 177 | [embedmd]:# (examples/texthandler/main.go /type textHandler/ /^}/) 178 | ```go 179 | type textHandler struct { 180 | h http.HandlerFunc 181 | } 182 | ``` 183 | 184 | Now we're going to define the `ServeHTTP` method on `textHandler` so it satisfies the `http.Handler` interface. 185 | 186 | [embedmd]:# (examples/texthandler/main.go /.*ServeHTTP/ /^}/) 187 | ```go 188 | func (t textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 189 | // Set the content type 190 | w.Header().Set("Content-Type", "text/plain") 191 | // Then call ServeHTTP in the decorated handler. 192 | t.h(w, r) 193 | } 194 | ``` 195 | 196 | Finally we replace our `http.HandleFunc` calls with `http.Handle`. 197 | 198 | [embedmd]:# (examples/texthandler/main.go /func main/ /^}/) 199 | ```go 200 | func main() { 201 | http.Handle("/hello", textHandler{helloHandler}) 202 | http.ListenAndServe(":8080", nil) 203 | } 204 | ``` 205 | 206 | #### Exercise Setting headers only once 207 | 208 | Modify the program from your previous exercise so you have only one line that set the content type of the response. 209 | 210 | #### Exercise Even better error handling (optional) 211 | 212 | Modify the `textHandler` that we showed before so instead of `http.HandlerFunc` it receives a function that 213 | returns an `int` and an `error`. The `ServeHTTP` method of `textHandler` should check use that integer and 214 | the `error` and set the status code and content accordingly. 215 | 216 | _Super mega optional_: define a new error type that contains the information about the status code too. 217 | The first to finish this might get a prize ... just saying. 218 | 219 | # Congratulations! 220 | 221 | You're now able to validate the input to your http handlers and set the status code and content type accordingly. 222 | You are clearly awesome! 🎉 223 | 224 | But you can be awesomer, I assure you, by going to [section 4](../section04/README.md). 225 | -------------------------------------------------------------------------------- /section07/README.md: -------------------------------------------------------------------------------- 1 | # 7: Durable storage with Cloud Datastore 2 | 3 | In this chapter you will learn the basics about the Google Cloud Datastore and 4 | how it is the perfect companion to your App Engine apps. 5 | 6 | ## Introduction to Cloud Datastore 7 | 8 | Cloud Datastore is a non relational database that is fully managed by Google so 9 | you don't need to worry about scaling, resizing, patches, or any other kind of 10 | maintenance. 11 | 12 |
13 | 14 |
15 | 16 | You can find more information on Cloud Datastore 17 | [here](https://cloud.google.com/datastore/docs/concepts/overview?hl=en) but for 18 | this tutorial just consider Cloud Datastore as the easiest way to store 19 | structured data from your App Engine app. 20 | 21 | ## Using Cloud Datastore from Go on App Engine 22 | 23 | The Go runtime on App Engine provides a suite of packages that give access to 24 | many of the services that you might want to use from a web application. One of 25 | those is Cloud Datastore, and the API is very well 26 | [documented](https://cloud.google.com/appengine/docs/go/datastore/). 27 | 28 | There are three concepts that you need to understand before you start using the 29 | Datastore: 30 | 31 | 1. entities, 32 | 1. kinds, and 33 | 1. keys. 34 | 35 | An entity is a value stored in the datastore. This is somewhat similar to a row 36 | stored in a relational database, it has a list of fields and its corresponding 37 | values. But unlike relational datastores there's no schema describing those rows 38 | and each entity can have a different set of fields. 39 | 40 | Entities are of a given _kind_, the same way rows in relational databases belong 41 | to a given table. Given the type `Person` that we used before we could imagine 42 | having a _kind_ `Person` in the datastore. 43 | 44 | All values in the datastore are stored attached to a key. Keys are the way to 45 | identify and refer to the values in the datastore. There are two types of keys: 46 | 47 | - complete keys: which actually point to a value in the datastore 48 | - incomplete keys: used when a value is still not in the datastore 49 | 50 | We'll see more about incomplete keys in a minute. 51 | 52 | # Storing data in the Datastore 53 | 54 | The [`google.golang.org/appengine/datastore`](https://godoc.org/google.golang.org/appengine/datastore) 55 | package provides a `Put` function: 56 | 57 | ```go 58 | func Put(c appengine.Context, key *Key, src interface{}) (*Key, error) 59 | ``` 60 | 61 | - The first parameter is an `appengine.Context` which is the way we link all the 62 | operations that are related to a given request together. 63 | 64 | - The second parameter is a `*datastore.Key` and you can see how to create 65 | one in the code snippet below. 66 | 67 | - Finally, the last parameter is the value to be stored. 68 | 69 | - The function returns another `*datastore.Key` and an error which will be 70 | non nil if some error occurs while storing the data. 71 | 72 | Let's see an example of how to use the `datastore.Put` function: 73 | 74 | 75 | [embedmd]:# (examples/app.go /func complete/ /^}/) 76 | ```go 77 | func completeHandler(w http.ResponseWriter, r *http.Request) { 78 | // create a new App Engine context from the HTTP request. 79 | ctx := appengine.NewContext(r) 80 | 81 | p := &Person{Name: "gopher", AgeYears: 5} 82 | 83 | // create a new complete key of kind Person and value gopher. 84 | key := datastore.NewKey(ctx, "Person", "gopher", 0, nil) 85 | // put p in the datastore. 86 | key, err := datastore.Put(ctx, key, p) 87 | if err != nil { 88 | http.Error(w, err.Error(), http.StatusInternalServerError) 89 | return 90 | } 91 | fmt.Fprintf(w, "gopher stored with key %v", key) 92 | } 93 | ``` 94 | 95 | Execute this code locally and observe how every time the handler is executed 96 | the value of the key is the same every time: `/Person,gopher`. 97 | 98 | As you can see we're creating a `datastore.Key` calling `datastore.NewKey`: 99 | 100 | ```go 101 | func NewKey(c appengine.Context, kind, stringID string, intID int64, parent *Key) *Key 102 | ``` 103 | 104 | When creating a complete key you need to choose between wheter you will use a 105 | `string` value (as in the example) or an `int64`. One and only one of the fields 106 | `stringID` and `intID` should be empty (`""` or `0`). 107 | 108 | The last parameter is a ```*datastore.Key``` pointing to the parent of the key 109 | we're creating. For now we will use always nil. 110 | 111 | ## Using incomplete keys 112 | 113 | What if we don't know the exact key we want to use? For instance different 114 | values of `Person` could have the same name and right their keys would be the 115 | same therefore overwriting each other. 116 | 117 | The solution is to use an autogenerated key, which is exactly what incomplete 118 | keys are for. We create them with `datastore.NewIncompletekey`: 119 | 120 | ```go 121 | func NewIncompleteKey(c appengine.Context, kind string, parent *Key) *Key 122 | ``` 123 | 124 | Note that there's no `stringID` or `intID` in this function, as the final value 125 | of the key will be decided once we put the value in the datastore. The final 126 | value can be obtained by using the returned key by `datastore.Put`. 127 | 128 | [embedmd]:# (examples/app.go /func incompleteHandler/ /^}/) 129 | ```go 130 | func incompleteHandler(w http.ResponseWriter, r *http.Request) { 131 | // create a new App Engine context from the HTTP request. 132 | ctx := appengine.NewContext(r) 133 | 134 | p := &Person{Name: "gopher", AgeYears: 5} 135 | 136 | // create a new complete key of kind Person. 137 | key := datastore.NewIncompleteKey(ctx, "Person", nil) 138 | // put p in the datastore. 139 | key, err := datastore.Put(ctx, key, p) 140 | if err != nil { 141 | http.Error(w, err.Error(), http.StatusInternalServerError) 142 | return 143 | } 144 | fmt.Fprintf(w, "gopher stored with key %v", key) 145 | } 146 | ``` 147 | 148 | If you use this handler in your application you will see that the generated key 149 | is a different one every time and it doesn't follow any specific order. 150 | 151 | # The Datastore admin page 152 | 153 | If you're running your application locally you can investigate the contents of 154 | the local datastore by visiting http://localhost:8000/datastore. 155 | 156 | 157 | 158 | If you want to investigate the content of the Cloud Datastore for an App Engine 159 | app deployed to production you can visit 160 | [this page](https://console.developers.google.com/project/_/datastore/stats). 161 | 162 | 163 | # Retrieving data from Google Cloud Datastore 164 | 165 | There are two main ways of retrieving data from the Google Cloud Datastore: 166 | 167 | - retrieving values given their keys, or 168 | - querying the datastore using filters. 169 | 170 | ## Retrieving with keys 171 | 172 | If we have a key, retrieving a value from the Datastore is as simple as calling 173 | the `datastore.Get` function: 174 | 175 | ```go 176 | func Get(c appengine.Context, key *Key, dst interface{}) error 177 | ``` 178 | 179 | The last parameter should be a pointer to a struct containing the fields that we 180 | want to retrieve, for instance: 181 | 182 | [embedmd]:# (examples/app.go /func getHandler/ /^}/) 183 | ```go 184 | func getHandler(w http.ResponseWriter, r *http.Request) { 185 | ctx := appengine.NewContext(r) 186 | 187 | key := datastore.NewKey(ctx, "Person", "gopher", 0, nil) 188 | 189 | var p Person 190 | err := datastore.Get(ctx, key, &p) 191 | if err != nil { 192 | http.Error(w, "Person not found", http.StatusNotFound) 193 | return 194 | } 195 | fmt.Fprintln(w, p) 196 | } 197 | ``` 198 | 199 | There's also a version of this function that allows us to retrieve multiple 200 | values at a time, therefore improving performance when we can batch operations. 201 | 202 | The function is `datastore.GetMulti`: 203 | 204 | ```go 205 | func GetMulti(c appengine.Context, key []*Key, dst interface{}) error 206 | ``` 207 | 208 | ## Retreiving without keys 209 | 210 | Very often we want to find all the values in the datastore that match some 211 | conditions, such as all the values of a _kind_, or all the values where a given 212 | field equals some specific value. This is similar to SQL `SELECT` statements. 213 | 214 | They way to do this with the Datastore is using the type `datastore.Query`. 215 | Values of `datastore.Query` can be created with `datastore.NewQuery`: 216 | 217 | ```go 218 | func NewQuery(kind string) *Query 219 | ``` 220 | 221 | As you can see all queries are attached to a given _kind_. Once we have a query 222 | we can add filters using the builder pattern and some of these methods: 223 | 224 | ```go 225 | func (q *Query) Ancestor(ancestor *Key) *Query 226 | ``` 227 | ```go 228 | func (q *Query) Filter(filterStr string, value interface{}) *Query 229 | ``` 230 | ```go 231 | func (q *Query) KeysOnly() *Query 232 | ``` 233 | ```go 234 | func (q *Query) Limit(limit int) *Query 235 | ``` 236 | ```go 237 | func (q *Query) Order(fieldName string) *Query 238 | ``` 239 | 240 | And finally we can get the values that match our query by executing the query: 241 | 242 | ```go 243 | func (q *Query) Count(c appengine.Context) (int, error) 244 | ``` 245 | ```go 246 | func (q *Query) GetAll(c appengine.Context, dst interface{}) ([]*Key, error) 247 | ``` 248 | ```go 249 | func (q *Query) Run(c appengine.Context) *Iterator 250 | ``` 251 | 252 | - Count returns how many values matched the query 253 | - GetAll retrieves all the values that matched the query into `dst` 254 | - Run returns a ```*datastore.Iterator``` that we can use to iterate over 255 | all the results matching the query. 256 | 257 | Let's see an example where we retrieve all the values of _kind_ `Person` that 258 | are 10 years old or younger ordered by their name. 259 | 260 | [embedmd]:# (examples/app.go /queryHandler/ /^}/) 261 | ```go 262 | queryHandler(w http.ResponseWriter, r *http.Request) { 263 | ctx := appengine.NewContext(r) 264 | 265 | var p []Person 266 | 267 | // create a new query on the kind Person 268 | q := datastore.NewQuery("Person") 269 | 270 | // select only values where field Age is 10 or lower 271 | q = q.Filter("Age <=", 10) 272 | 273 | // order all the values by the Name field 274 | q = q.Order("Name") 275 | 276 | // and finally execute the query retrieving all values into p. 277 | _, err := q.GetAll(ctx, &p) 278 | if err != nil { 279 | http.Error(w, err.Error(), http.StatusInternalServerError) 280 | return 281 | } 282 | fmt.Fprintln(w, p) 283 | } 284 | ``` 285 | 286 | Note that `Filter` and `Order` return a `*datastore.Query`. This means you can 287 | chain those operations into a single expression. 288 | 289 | [embedmd]:# (examples/app.go /q :=.*\.$/ /^$/) 290 | ```go 291 | q := datastore.NewQuery("Person"). 292 | Filter("Age <=", 10). 293 | Order("Name") 294 | ``` 295 | 296 | 297 | You can find more information about Datastore Query 298 | [here](https://cloud.google.com/appengine/docs/go/datastore/queries). 299 | 300 | # Exercise: add durable storage to the events app 301 | 302 | Now that you know everything that there is to know (for today) for the Datastore, 303 | let's use it to make the data stored in the events application durable. 304 | 305 | Go work on the [step two](../events/step2/README.md) and come back here when you're done. 306 | 307 | # Congratulations! 308 | 309 | You are now able to store and retrieve data from the Google Cloud Datastore. 310 | With this you're now ready to build pretty much any application you can imagine! 311 | 312 | Or do you want more? In that case go the 313 | [next chapter](../section08/README.md) to learn how to access remote 314 | resources using `urlfetch`. 315 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------