Posts for: #Golang

Multiple Site

自從有了 Let’s Encrypt 以後,網站的 ssl 憑證都是用他們家

免費的憑証,原本的設定都是把 nginx 擺在前面,利用 virtual host

的方式,可以對應到多個站點,用 acme.sh

command line 的工具去管理,多個站點的 ssl 憑証更新,時間久了,還是

覺得麻煩,後來乾脆,直接只跑一個 golang 的 server 同時 host 多個站點

golang crawler demo

最近有個面試的回家作業,做了一個簡單的爬蟲

單純只有爬一下,家樂福,還有大潤發的網站,一家是用 HTML 不斷的翻頁下去,爬完休息
另一家,直接找他的 json api ,直接抓 api 的結果就可以了

目前整個 code base 挑出來的毛病,或是說,用一個微小的專案,來看軟體開發這件事,需要具備的東西
也是一個不錯的自我成長方式,可以一步一步看,目前用 golang 來實作一些,像是後端服務的一些處理模式

會寫出來,主要是也許會對剛好有機會碰到,這些問題的人有幫助

code reference https://github.com/terryh/honestman

我先列一些一般來說用 golang 來寫一些像是 api 服務的問題,或是比較像是軟體的架構模式來討論

1) crawler (爬蟲) 的 go routine 模式

2) 要用 ORM 來抽象化 SQL 的操作好,還是不用 ORM 來操作

3) 你需要用 framework 嗎?

4) simple test case and benchmark ?


由於,這其實是一個可大可小的題目,但是,我還是來簡單說一下,目前,一般用 golang 做 web service ,或是給手遊用的
後端服務介面,或是手機應用程式的 api 介面,大概會用哪一些函式庫

如果,開發團隊人員不多,對整個 http/s 或是網路服務認識不多,會選,既有的框架
目前,比較受歡迎的有

https://github.com/gin-gonic/gin

https://github.com/astaxie/beego

https://github.com/labstack/echo

我個人比較推薦是相容於 golang http library http.HandlerFunc 的 framework

如果不要框架,選擇更多,但是,這種人必須比較專業,也要更了解要處理的問題,還有 golang standard library ,因為,要有能力可以判斷,
像大家建議的 COMPOSITION 方式 https://www.youtube.com/watch?v=194blNHDdd0

自己組建比較多,就是選用一個 http router dispacher
這個則是有很多的參考,或是選擇 https://github.com/julienschmidt/go-http-routing-benchmark

剩下的大概就是組建自己的服務了

需要 HTML, JSON render 的,或是需要驗證身份,或是希望引用 ORM 來達到和 DB 層的抽象化,這些每一種都一個選擇,伴
隨著也都是取捨的問題

未完待續,接下來,上面列出的點,會做作小筆記,也希望對需要的人有幫助 ;-)

web backend service in golang

先前後端開發 web service 大多是以小型的 web framework echo 來做基礎開發

https://github.com/labstack/echo

到了後來,因為他新版 v2 api 的介面修改,包了另一個 http engine fasthttp 的模組,雖然多了一層的彈性

多了一個選擇,但是在 golang 1.7 版以後,標準的函式庫已經包含了 context ,所有 framework 要解決的問題

幾乎都可以用標準函式庫,就全可以解決了,也可以真正體會 composition 組件式的架構方式,第三方的函式也更容易整併

請務必試試看,用 golang 標準的函式庫,加上一些第三方的函式,組建 web service

更新待續

How I deploy my golang binary

在網路上,有很多 Docker 的佈署的文章,或是用來測試的方式

用 golang 撰寫的服務,編譯成執行檔後,你可以有很多的選擇,例如:upstart , supervisord , 或是 daemontools

這一次,我想用 Docker ,想要他的彈性

但是,不管用 Ubuntu , Debian 或是官方的 Golang 的 image

總覺得 image 太大,不合用,直接由 scratch 做 image 又覺得,萬一有一些系統的相依套件,需要的時候,不方便

所以這是我目前的萬用 image 的 Dockerfile


FROM alpine:3.2
RUN apk –update add curl
WORKDIR /usr/src/app
ENTRYPOINT ["/usr/src/app/goapp"]

實際的編譯執行檔,分資料夾放,名字都叫做 goapp,執行 container 的時候,用 -v 掛載進 container 的 /usr/src/app
golang 的程式記得可以用環境變數,更改一些執行的變數,像是,資料庫連結,等等,只要執行 container 時,用 -e 帶給 container

以後上新版的執行檔,就只要下 docker restart your_running_container 而已


image 裡安裝的 curl ,只是要順便把 ssl certificates 安裝,這樣就有 root certificates

這樣的 images 大小大約 8.7 MB ,夠小,也很快

golang routers

覺得簡單易懂的好文章

可以讓你用 golang 寫 api 有一個簡單的開始

http://nicolasmerouze.com/guide-routers-golang/

另外也推薦他的 gist ,再把 main 的部份,拔出來,做一個 NewRouter 應該更好測試,再加幾個函式庫,就是一個 micro framework 了

作者這一系列的文章 http://nicolasmerouze.com/build-web-framework-golang/

package main
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/context"
"github.com/julienschmidt/httprouter"
"github.com/justinas/alice"
)
func recoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %+v", err)
http.Error(w, http.StatusText(500), 500)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func loggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
next.ServeHTTP(w, r)
t2 := time.Now()
log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}
return http.HandlerFunc(fn)
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You are on the about page.")
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome!")
}
type appContext struct {
db *sql.DB
}
func (c *appContext) authHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
authToken := r.Header.Get("Authorization")
user, err := map[string]interface{}{}, errors.New("test")
// user, err := getUser(c.db, authToken)
log.Println(authToken)
if err != nil {
http.Error(w, http.StatusText(401), 401)
return
}
context.Set(r, "user", user)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func (c *appContext) adminHandler(w http.ResponseWriter, r *http.Request) {
user := context.Get(r, "user")
// Maybe other operations on the database
json.NewEncoder(w).Encode(user)
}
func (c *appContext) teaHandler(w http.ResponseWriter, r *http.Request) {
params := context.Get(r, "params").(httprouter.Params)
log.Println(params.ByName("id"))
// tea := getTea(c.db, params.ByName("id"))
json.NewEncoder(w).Encode(nil)
}
// We could also put *httprouter.Router in a field to not get access to the original methods (GET, POST, etc. in uppercase)
type router struct {
*httprouter.Router
}
func (r *router) Get(path string, handler http.Handler) {
r.GET(path, wrapHandler(handler))
}
func NewRouter() *router {
return &router{httprouter.New()}
}
func wrapHandler(h http.Handler) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
context.Set(r, "params", ps)
h.ServeHTTP(w, r)
}
}
func main() {
// db := sql.Open("postgres", "...")
appC := appContext{nil}
commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler)
router := NewRouter()
router.Get("/admin", commonHandlers.Append(appC.authHandler).ThenFunc(appC.adminHandler))
router.Get("/about", commonHandlers.ThenFunc(aboutHandler))
router.Get("/", commonHandlers.ThenFunc(indexHandler))
router.Get("/teas/:id", commonHandlers.ThenFunc(appC.teaHandler))
http.ListenAndServe(":8080", router)
}
view raw main.go hosted with ❤ by GitHub


package main
import (
"encoding/json"
"log"
"net/http"
"reflect"
"time"
"github.com/gorilla/context"
"github.com/julienschmidt/httprouter"
"github.com/justinas/alice"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
// Repo
type Tea struct {
Id bson.ObjectId `json:"id,omitempty" bson:"_id,omitempty"`
Name string `json:"name"`
Category string `json:"category"`
}
type TeasCollection struct {
Data []Tea `json:"data"`
}
type TeaResource struct {
Data Tea `json:"data"`
}
type TeaRepo struct {
coll *mgo.Collection
}
func (r *TeaRepo) All() (TeasCollection, error) {
result := TeasCollection{[]Tea{}}
err := r.coll.Find(nil).All(&result.Data)
if err != nil {
return result, err
}
return result, nil
}
func (r *TeaRepo) Find(id string) (TeaResource, error) {
result := TeaResource{}
err := r.coll.FindId(bson.ObjectIdHex(id)).One(&result.Data)
if err != nil {
return result, err
}
return result, nil
}
func (r *TeaRepo) Create(tea *Tea) error {
id := bson.NewObjectId()
_, err := r.coll.UpsertId(id, tea)
if err != nil {
return err
}
tea.Id = id
return nil
}
func (r *TeaRepo) Update(tea *Tea) error {
err := r.coll.UpdateId(tea.Id, tea)
if err != nil {
return err
}
return nil
}
func (r *TeaRepo) Delete(id string) error {
err := r.coll.RemoveId(bson.ObjectIdHex(id))
if err != nil {
return err
}
return nil
}
// Errors
type Errors struct {
Errors []*Error `json:"errors"`
}
type Error struct {
Id string `json:"id"`
Status int `json:"status"`
Title string `json:"title"`
Detail string `json:"detail"`
}
func WriteError(w http.ResponseWriter, err *Error) {
w.Header().Set("Content-Type", "application/vnd.api+json")
w.WriteHeader(err.Status)
json.NewEncoder(w).Encode(Errors{[]*Error{err}})
}
var (
ErrBadRequest = &Error{"bad_request", 400, "Bad request", "Request body is not well-formed. It must be JSON."}
ErrNotAcceptable = &Error{"not_acceptable", 406, "Not Acceptable", "Accept header must be set to 'application/vnd.api+json'."}
ErrUnsupportedMediaType = &Error{"unsupported_media_type", 415, "Unsupported Media Type", "Content-Type header must be set to: 'application/vnd.api+json'."}
ErrInternalServer = &Error{"internal_server_error", 500, "Internal Server Error", "Something went wrong."}
)
// Middlewares
func recoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %+v", err)
WriteError(w, ErrInternalServer)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func loggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
t1 := time.Now()
next.ServeHTTP(w, r)
t2 := time.Now()
log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}
return http.HandlerFunc(fn)
}
func acceptHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Accept") != "application/vnd.api+json" {
WriteError(w, ErrNotAcceptable)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func contentTypeHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-Type") != "application/vnd.api+json" {
WriteError(w, ErrUnsupportedMediaType)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func bodyHandler(v interface{}) func(http.Handler) http.Handler {
t := reflect.TypeOf(v)
m := func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
val := reflect.New(t).Interface()
err := json.NewDecoder(r.Body).Decode(val)
if err != nil {
WriteError(w, ErrBadRequest)
return
}
if next != nil {
context.Set(r, "body", val)
next.ServeHTTP(w, r)
}
}
return http.HandlerFunc(fn)
}
return m
}
// Main handlers
type appContext struct {
db *mgo.Database
}
func (c *appContext) teasHandler(w http.ResponseWriter, r *http.Request) {
repo := TeaRepo{c.db.C("teas")}
teas, err := repo.All()
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/vnd.api+json")
json.NewEncoder(w).Encode(teas)
}
func (c *appContext) teaHandler(w http.ResponseWriter, r *http.Request) {
params := context.Get(r, "params").(httprouter.Params)
repo := TeaRepo{c.db.C("teas")}
tea, err := repo.Find(params.ByName("id"))
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/vnd.api+json")
json.NewEncoder(w).Encode(tea)
}
func (c *appContext) createTeaHandler(w http.ResponseWriter, r *http.Request) {
body := context.Get(r, "body").(*TeaResource)
repo := TeaRepo{c.db.C("teas")}
err := repo.Create(&body.Data)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/vnd.api+json")
w.WriteHeader(201)
json.NewEncoder(w).Encode(body)
}
func (c *appContext) updateTeaHandler(w http.ResponseWriter, r *http.Request) {
params := context.Get(r, "params").(httprouter.Params)
body := context.Get(r, "body").(*TeaResource)
body.Data.Id = bson.ObjectIdHex(params.ByName("id"))
repo := TeaRepo{c.db.C("teas")}
err := repo.Update(&body.Data)
if err != nil {
panic(err)
}
w.WriteHeader(204)
w.Write([]byte("\n"))
}
func (c *appContext) deleteTeaHandler(w http.ResponseWriter, r *http.Request) {
params := context.Get(r, "params").(httprouter.Params)
repo := TeaRepo{c.db.C("teas")}
err := repo.Delete(params.ByName("id"))
if err != nil {
panic(err)
}
w.WriteHeader(204)
w.Write([]byte("\n"))
}
// Router
type router struct {
*httprouter.Router
}
func (r *router) Get(path string, handler http.Handler) {
r.GET(path, wrapHandler(handler))
}
func (r *router) Post(path string, handler http.Handler) {
r.POST(path, wrapHandler(handler))
}
func (r *router) Put(path string, handler http.Handler) {
r.PUT(path, wrapHandler(handler))
}
func (r *router) Delete(path string, handler http.Handler) {
r.DELETE(path, wrapHandler(handler))
}
func NewRouter() *router {
return &router{httprouter.New()}
}
func wrapHandler(h http.Handler) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
context.Set(r, "params", ps)
h.ServeHTTP(w, r)
}
}
func main() {
session, err := mgo.Dial("localhost")
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
appC := appContext{session.DB("test")}
commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler, acceptHandler)
router := NewRouter()
router.Get("/teas/:id", commonHandlers.ThenFunc(appC.teaHandler))
router.Put("/teas/:id", commonHandlers.Append(contentTypeHandler, bodyHandler(TeaResource{})).ThenFunc(appC.updateTeaHandler))
router.Delete("/teas/:id", commonHandlers.ThenFunc(appC.deleteTeaHandler))
router.Get("/teas", commonHandlers.ThenFunc(appC.teasHandler))
router.Post("/teas", commonHandlers.Append(contentTypeHandler, bodyHandler(TeaResource{})).ThenFunc(appC.createTeaHandler))
http.ListenAndServe(":8080", router)
}
view raw main.go hosted with ❤ by GitHub



相關也推再看 alice 模組作者的文章,雖然 alice 很單純,單純到自己刻,可能也只有 5 行左右
不過看一下,作者的實作概念,挺不錯的


https://justinas.org/alice-painless-middleware-chaining-for-go/
https://github.com/justinas/alice
https://github.com/julienschmidt/httprouter
https://github.com/gorilla/context

Write in GO

感覺最近好像蠻常分享 GO 的東西 ;-)

今年時間剩下幾個月及接下來的一年的時間,想再多做一點 Android 的東西,還有集群架構的服務,該用 Python, javascript, Golang 的地方,當然就給他直上

另外 jinja 作者極力推坑 的 Rust 有時間
可以來把玩一下

現在我也是 Mac 的使用者,對他的軟體,硬體,覺得真的一直在進步,不過,對於 Apple Developer 必須要繳年費,搞憑證
開發人員要付錢,然後做免費的 App 幫 Apple 擴張市場,就算是付得起,也超不爽的,非常不以為然,這根本是裝笑維

iOS push notification

首先您必須到 https://developer.apple.com/ 去新增一個 App,一般就是你要用 push notification 的應用程式

設定好名字,還有 ID ,記得要把 Push Notifications 的服務打勾

我這裡說明就用中文,因為英文的說明有一堆了

接下來就可以打開您的 Mac / 應用程式 / 工具程式 / 鑰匙圈存取

我們選憑證輔助程式,從憑證授權要求憑證




把他存下來,我舉的例子就叫 PushMsg ,把檔名改成 PushMsg 所以你會得到 PushMsg.certSigningRequest 這一個 CSR ,待會要到 apple 網站上面產生憑證



產生後,到鑰匙圈存取 應用程式,選鑰匙的地方,你會看到有 PushMsg 的公鑰和密鑰,點選專用密鑰,按下滑鼠右鍵輸出憑證,輸出成 PushMsg.p12 待會用










這時候,請用剛剛的 PushMsg.certSigningRequest 到 Apple 開發者網站的 App 設定 notification 的地方去產生憑證,可以下載後,

開發模式,你會得到 aps_development.cer ,實際部署模式,請依此類推



我們現在有 PushMsg.certSigningRequest , PushMsg.p12 , 還有 aps_development.cer 接下來,我們來產生,Server 端程式需要的憑證格式

這裡的程式,我們用 Golang 的範例,其他用 Python , Node.js 或是其他語言的朋友請依此類推

產生 cert.pem

openssl x509 -in aps_development.cer -inform der -out cert.pem

產生 key.pem 由於我很懶,key.pem 不想加密碼,程式不想再判斷密碼部分,我用這樣,如果你要加 pass phrase 就不要加上 -nodes 即可
openssl pkcs12 -in PushMsg.p12 -out key.pem -nodes

最後就是用 Apple 文件上面的 openssl 測試即可,沒有錯誤,就是 OK 了
openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert cert.pem -key key.pem 

最後附上 golang 的測試程式,實際上已經有現成的 library 可以用 https://github.com/anachronistic/apns

只有連線的測試,有用 openssl 測試就可了,直接執行沒有錯誤,就是連線沒有問題了,送訊息可以直接用上面的 library

package mainimport ( “crypto/tls” “fmt” “net” “os”)func main() { // load  cert, err := tls.LoadX509KeyPair("./cert.pem", “./key.pem”) if err != nil {  fmt.Println(“key error: “, err)  os.Exit(1) } conf := &tls.Config{  Certificates: []tls.Certificate{cert},  ServerName:   “gateway.sandbox.push.apple.com”, } // connect to APPLE  conn, err := net.Dial(“tcp”, “gateway.sandbox.push.apple.com:2195”) if err != nil {  fmt.Println(“tcp error: “, err)  os.Exit(1) } tlsconn := tls.Client(conn, conf)        // be nice err = tlsconn.Handshake() if err != nil {  fmt.Println(“tls error: “, err)  os.Exit(1) }}



參考資料

http://www.raywenderlich.com/32960/apple-push-notification-services-in-ios-6-tutorial-part-1

https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html

http://bravenewmethod.com/2011/02/25/apple-push-notifications-with-go-language/

groupcache

這一個 golang 裡面非常帥氣的 library ,作者是原 memcached 的作者之一,主要就是讓 cache 機制再簡化

集群的部署能力也還不錯,完全不用再管 server instance ,我對這種,以簡單為名的 library 最沒有招架能力了

https://github.com/golang/groupcache

使用上也是非常的帥氣,可以參考 OSCON 的簡報

http://talks.golang.org/2013/oscon-dl.slide#1

詳細的使用上可以參考, source code ,或是 test case,還有很多種用法

以下是我用的範例,您可以用在很多地方,像是外部 url fetch ,或是很重的 query 或是檔案 IO 之類的
就是平常,你 cache 怎麼用,就可以怎麼用,cache 只有支援 拿,和寫,

以下的例子用這樣拿資料,我的 cache key 大概到小時,就用自己算每小時不同的 key
FileInfoCache.Get(nil, cacheFileInfo, groupcache.AllocatingByteSliceSink(&data))

另外,FileInfoFetch 就是另外一個做很多 disk IO 的工作
dest.SetBytes(FileInfoFetch(path))


// Example for groupcache// snip from my codevar (        FileInfoCache  *groupcache.Group)func init(){        //////////////////////////////////////////        // init book query cache        //cacheAddr := “127.0.0.1:55555”        //peers := groupcache.NewHTTPPool(“http://” + cacheAddr)        // the cache key is compose with timestamp hour        // 2006-01-02 15!!!url        getter := groupcache.GetterFunc(func(ctx groupcache.Context, key string, dest groupcache.Sink) error {                keys := strings.SplitN(key, “!!!”, 2)                //ts := keys[0]                path := keys[1]                dest.SetBytes(FileInfoFetch(path))                return nil        })        if FileInfoCache == nil {                // cache not been init                // allocate 64 MB memory for groupcache                FileInfoCache = groupcache.NewGroup(“FileInfo”, 64«20, getter)        }        }


你看,帥不帥氣,完全不需要另外跑,cache server daemon ,如果要組成 cluster 就直接在程式裡用 HTTPPool 的服務