Dopo un recente confronto tra Python, Ruby e Golang per un'applicazione a riga di comando, ho deciso di utilizzare lo stesso schema per confrontare la creazione di un semplice servizio web. Ho selezionato Flask (Python), Sinatra (Ruby) e Martini (Golang) per questo confronto. Sì, ci sono molte altre opzioni per le librerie di applicazioni web in ogni lingua, ma ho sentito che queste tre si prestano bene al confronto.
Panoramica della libreria
Ecco un confronto di alto livello delle librerie di Stackshare.
Boccetta (Pitone)
Flask è un micro-framework per Python basato su Werkzeug, Jinja2 e buone intenzioni.
Per applicazioni molto semplici, come quella mostrata in questa demo, Flask è un'ottima scelta. L'applicazione di base Flask contiene solo 7 righe di codice (LOC) in un unico file sorgente Python. Il vantaggio di Flask rispetto ad altre librerie Web Python (come Django o Pyramid) è che puoi iniziare in piccolo e creare un'applicazione più complessa secondo necessità.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
Sinatra (Rubino)
Sinatra è un DSL per creare rapidamente applicazioni web in Ruby con il minimo sforzo.
Proprio come Flask, Sinatra è ottimo per applicazioni semplici. L'applicazione di base Sinatra è solo 4 LOC in un unico file sorgente Ruby. Sinatra viene utilizzato al posto di librerie come Ruby on Rails per lo stesso motivo di Flask:puoi iniziare in piccolo ed espandere l'applicazione secondo necessità.
require 'sinatra'
get '/hi' do
"Hello World!"
end
Martini (Golang)
Martini è un potente pacchetto per scrivere rapidamente applicazioni/servizi web modulari in Golang.
Martini viene fornito con alcune batterie in più incluse rispetto a Sinatra e Flask, ma è ancora molto leggero per cominciare - solo 9 LOC per l'applicazione di base. Martini è stato oggetto di alcune critiche da parte della comunità di Golang, ma ha ancora uno dei progetti Github più apprezzati di qualsiasi framework web Golang. L'autore di Martini ha risposto qui direttamente alla critica. Alcuni altri framework includono Revel, Gin e persino la libreria net/http integrata.
package main
import "github.com/go-martini/martini"
func main() {
m := martini.Classic()
m.Get("/", func() string {
return "Hello world!"
})
m.Run()
}
Con le basi fuori mano, costruiamo un'app!
Descrizione del servizio
Il servizio creato fornisce un'applicazione blog molto semplice. Vengono costruiti i seguenti percorsi:
GET /
:Restituisci il blog (usando un modello per il rendering).GET /json
:restituisce il contenuto del blog in formato JSON.POST /new
:aggiungi un nuovo post (titolo, riepilogo, contenuto) al blog.
L'interfaccia esterna al servizio blog è esattamente la stessa per ogni lingua. Per semplicità MongoDB verrà utilizzato come archivio dati per questo esempio poiché è il più semplice da configurare e non dobbiamo preoccuparci affatto degli schemi. In una normale applicazione "simile a un blog" sarebbe probabilmente necessario un database relazionale.
Aggiungi un post
POST /new
$ curl --form title='Test Post 1' \
--form summary='The First Test Post' \
--form content='Lorem ipsum dolor sit amet, consectetur ...' \
http://[IP]:[PORT]/new
Visualizza l'HTML
GET /
Visualizza il JSON
GET /json
[
{
content:"Lorem ipsum dolor sit amet, consectetur ...",
title:"Test Post 1",
_id:{
$oid:"558329927315660001550970"
},
summary:"The First Test Post"
}
]
Struttura dell'applicazione
Ogni applicazione può essere suddivisa nei seguenti componenti:
Configurazione dell'applicazione
- Inizia un'applicazione
- Esegui l'applicazione
Richiesta
- Definire percorsi su cui un utente può richiedere dati (GET)
- Definire percorsi su cui un utente può inviare dati (POST)
Risposta
- Renderizza JSON (
GET /json
) - Renderizzare un modello (
GET /
)
Banca dati
- Inizia una connessione
- Inserisci dati
- Recupera dati
Distribuzione dell'applicazione
- Docker!
Il resto di questo articolo confronterà ciascuno di questi componenti per ciascuna libreria. Lo scopo non è suggerire che una di queste librerie sia migliore dell'altra, ma fornire un confronto specifico tra i tre strumenti:
- Boccetta (Pitone)
- Sinatra (Rubino)
- Martini (Golang)
Impostazione progetto
Tutti i progetti vengono avviati utilizzando docker e docker-compose. Prima di approfondire il modo in cui ogni applicazione viene avviata sotto il cofano, possiamo semplicemente usare la finestra mobile per farla funzionare esattamente allo stesso modo - docker-compose up
Seriamente, questo è tutto! Ora per ogni applicazione c'è un Dockerfile
e un docker-compose.yml
file che specifica cosa succede quando esegui il comando precedente.
Python (flask) - File Docker
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
Questo Dockerfile
dice che stiamo partendo da un'immagine di base con Python 3.4 installato, aggiungendo la nostra applicazione a /app
directory e utilizzando pip per installare i requisiti dell'applicazione specificati in requirements.txt
.
Rubino (sinatra)
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
Questo Dockerfile
dice che stiamo partendo da un'immagine di base con Ruby 2.2 installato, aggiungendo la nostra applicazione a /app
directory e utilizzando bundler per installare i requisiti dell'applicazione specificati nel Gemfile
.
Golang (martini)
FROM golang:1.3
ADD . /go/src/github.com/kpurdon/go-blog
WORKDIR /go/src/github.com/kpurdon/go-blog
RUN go get github.com/go-martini/martini && \
go get github.com/martini-contrib/render && \
go get gopkg.in/mgo.v2 && \
go get github.com/martini-contrib/binding
Questo Dockerfile
dice che stiamo partendo da un'immagine di base con Golang 1.3 installato, aggiungendo la nostra applicazione al /go/src/github.com/kpurdon/go-blog
directory e ottenere tutte le nostre dipendenze necessarie utilizzando go get
comando.
Inizializza/Esegui un'applicazione
Python (flask) - app.py
# initialize application
from flask import Flask
app = Flask(__name__)
# run application
if __name__ == '__main__':
app.run(host='0.0.0.0')
$ python app.py
Rubino (Sinatra) - app.rb
# initialize application
require 'sinatra'
$ ruby app.rb
Golang (Martini) - app.go
// initialize application
package main
import "github.com/go-martini/martini"
import "github.com/martini-contrib/render"
func main() {
app := martini.Classic()
app.Use(render.Renderer())
// run application
app.Run()
}
$ go run app.go
Definisci un percorso (GET/POST)
Python (boccetta)
# get
@app.route('/') # the default is GET only
def blog():
# ...
#post
@app.route('/new', methods=['POST'])
def new():
# ...
Rubino (Sinatra)
# get
get '/' do
# ...
end
# post
post '/new' do
# ...
end
Golang (Martini)
// define data struct
type Post struct {
Title string `form:"title" json:"title"`
Summary string `form:"summary" json:"summary"`
Content string `form:"content" json:"content"`
}
// get
app.Get("/", func(r render.Render) {
// ...
}
// post
import "github.com/martini-contrib/binding"
app.Post("/new", binding.Bind(Post{}), func(r render.Render, post Post) {
// ...
}
Esegui il rendering di una risposta JSON
Python (boccetta)
Flask fornisce un metodo jsonify() ma poiché il servizio utilizza MongoDB viene utilizzata l'utilità mongodb bson.
from bson.json_util import dumps
return dumps(posts) # posts is a list of dicts [{}, {}]
Rubino (Sinatra)
require 'json'
content_type :json
posts.to_json # posts is an array (from mongodb)
Golang (Martini)
r.JSON(200, posts) // posts is an array of Post{} structs
Renderizzare una risposta HTML (modello)
Python (boccetta)
return render_template('blog.html', posts=posts)
<!doctype HTML>
<html>
<head>
<title>Python Flask Example</title>
</head>
<body>
{% for post in posts %}
<h1> {{ post.title }} </h1>
<h3> {{ post.summary }} </h3>
<p> {{ post.content }} </p>
<hr>
{% endfor %}
</body>
</html>
Rubino (Sinatra)
erb :blog
<!doctype HTML>
<html>
<head>
<title>Ruby Sinatra Example</title>
</head>
<body>
<% @posts.each do |post| %>
<h1><%= post['title'] %></h1>
<h3><%= post['summary'] %></h3>
<p><%= post['content'] %></p>
<hr>
<% end %>
</body>
</html>
Golang (Martini)
r.HTML(200, "blog", posts)
<!doctype HTML>
<html>
<head>
<title>Golang Martini Example</title>
</head>
<body>
{{range . }}
<h1>{{.Title}}</h1>
<h3>{{.Summary}}</h3>
<p>{{.Content}}</p>
<hr>
{{ end }}
</body>
</html>
Connessione al database
Tutte le applicazioni utilizzano il driver mongodb specifico per la lingua. La variabile di ambiente DB_PORT_27017_TCP_ADDR
è l'IP di un contenitore Docker collegato (l'IP del database).
Python (boccetta)
from pymongo import MongoClient
client = MongoClient(os.environ['DB_PORT_27017_TCP_ADDR'], 27017)
db = client.blog
Rubino (Sinatra)
require 'mongo'
db_ip = [ENV['DB_PORT_27017_TCP_ADDR']]
client = Mongo::Client.new(db_ip, database: 'blog')
Golang (Martini)
import "gopkg.in/mgo.v2"
session, _ := mgo.Dial(os.Getenv("DB_PORT_27017_TCP_ADDR"))
db := session.DB("blog")
defer session.Close()
Inserisci dati da un POST
Python (boccetta)
from flask import request
post = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
db.blog.insert_one(post)
Rubino (Sinatra)
client[:posts].insert_one(params) # params is a hash generated by sinatra
Golang (Martini)
db.C("posts").Insert(post) // post is an instance of the Post{} struct
Recupera dati
Python (boccetta)
posts = db.blog.find()
Rubino (Sinatra)
@posts = client[:posts].find.to_a
Golang (Martini)
var posts []Post
db.C("posts").Find(nil).All(&posts)
Distribuzione dell'applicazione (Docker!)
Un'ottima soluzione per distribuire tutte queste applicazioni consiste nell'usare la finestra mobile e la composizione mobile.
Python (boccetta)
File Docker
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
docker-compose.yml
web:
build: .
command: python -u app.py
ports:
- "5000:5000"
volumes:
- .:/app
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Rubino (Sinatra)
File Docker
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
docker-compose.yml
web:
build: .
command: bundle exec ruby app.rb
ports:
- "4567:4567"
volumes:
- .:/app
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Golang (Martini)
File Docker
FROM golang:1.3
ADD . /go/src/github.com/kpurdon/go-todo
WORKDIR /go/src/github.com/kpurdon/go-todo
RUN go get github.com/go-martini/martini && go get github.com/martini-contrib/render && go get gopkg.in/mgo.v2 && go get github.com/martini-contrib/binding
docker-compose.yml
web:
build: .
command: go run app.go
ports:
- "3000:3000"
volumes: # look into volumes v. "ADD"
- .:/go/src/github.com/kpurdon/go-todo
links:
- db
db:
image: mongo:3.0.4
command: mongod --quiet --logpath=/dev/null
Conclusione
Per concludere, diamo un'occhiata a quelle che ritengo siano alcune categorie in cui le biblioteche presentate si separano l'una dall'altra.
Semplicità
Mentre Flask è molto leggero e si legge chiaramente, l'app Sinatra è la più semplice delle tre a 23 LOC (rispetto a 46 per Flask e 42 per Martini). Per questi motivi Sinatra è il vincitore in questa categoria. Va notato, tuttavia, che la semplicità di Sinatra è dovuta a una "magia" più predefinita, ad esempio il lavoro implicito che avviene dietro le quinte. Per i nuovi utenti questo può spesso creare confusione.
Ecco un esempio specifico di "magia" in Sinatra:
params # the "request.form" logic in python is done "magically" behind the scenes in Sinatra.
E il codice Flask equivalente:
from flask import request
params = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
Per i principianti la programmazione di Flask e Sinatra è certamente più semplice, ma per un programmatore esperto con tempo speso in altri linguaggi tipizzati staticamente Martini fornisce un'interfaccia abbastanza semplicistica.
Documentazione
La documentazione di Flask era la più semplice da cercare e la più accessibile. Sebbene Sinatra e Martini siano entrambi ben documentati, la documentazione stessa non era così accessibile. Per questo Flask è il vincitore in questa categoria.
Comunità
Flask è il vincitore senza dubbio in questa categoria. La comunità di Ruby è il più delle volte dogmatica sul fatto che Rails sia l'unica buona scelta se hai bisogno di qualcosa di più di un servizio di base (anche se Padrino offre questo oltre a Sinatra). La comunità di Golang non è ancora lontana dal consenso su uno (o anche solo pochi) framework web, il che è prevedibile dato che la lingua stessa è così giovane. Python, tuttavia, ha adottato una serie di approcci allo sviluppo web, tra cui Django per applicazioni web complete e pronte all'uso e Flask, Bottle, CheryPy e Tornado per un approccio micro-framework.
Determinazione finale
Si noti che lo scopo di questo articolo non era promuovere un singolo strumento, ma fornire un confronto imparziale tra Flask, Sinatra e Martini. Detto questo, sceglierei Flask (Python) o Sinatra (Ruby). Se provieni da un linguaggio come C o Java, forse la natura staticamente tipizzata di Golang potrebbe attirarti. Se sei un principiante, Flask potrebbe essere la scelta migliore in quanto è molto facile da usare e c'è pochissima "magia" predefinita. Il mio consiglio è di essere flessibile nelle decisioni quando selezioni una libreria per il tuo progetto.
Domande? Risposta? Si prega di commentare di seguito. Grazie!
Inoltre, facci sapere se sei interessato a vedere alcuni benchmark.