Efter en nylig sammenligning af Python, Ruby og Golang for en kommandolinjeapplikation besluttede jeg at bruge det samme mønster til at sammenligne opbygning af en simpel webtjeneste. Jeg har valgt Flask (Python), Sinatra (Ruby) og Martini (Golang) til denne sammenligning. Ja, der er mange andre muligheder for webapplikationsbiblioteker på hvert sprog, men jeg følte, at disse tre egner sig godt til sammenligning.
Bibliotek oversigter
Her er en sammenligning på højt niveau af bibliotekerne fra Stackshare.
Flaske (Python)
Flask er en mikroramme for Python baseret på Werkzeug, Jinja2 og gode intentioner.
Til meget simple applikationer, såsom den, der er vist i denne demo, er Flask et godt valg. Den grundlæggende Flask-applikation er kun 7 linjer kode (LOC) i en enkelt Python-kildefil. Fordelen med Flask i forhold til andre Python-webbiblioteker (såsom Django eller Pyramid) er, at du kan starte i det små og bygge op til en mere kompleks applikation efter behov.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
Sinatra (Ruby)
Sinatra er en DSL til hurtigt at skabe webapplikationer i Ruby med minimal indsats.
Ligesom Flask er Sinatra fantastisk til simple applikationer. Den grundlæggende Sinatra-applikation er kun 4 LOC i en enkelt Ruby-kildefil. Sinatra bruges i stedet for biblioteker såsom Ruby on Rails af samme grund som Flask - du kan starte i det små og udvide applikationen efter behov.
require 'sinatra'
get '/hi' do
"Hello World!"
end
Martini (Golang)
Martini er en kraftfuld pakke til hurtigt at skrive modulære webapplikationer/tjenester i Golang.
Martini kommer med et par flere batterier inkluderet end både Sinatra og Flask, men er stadig meget let til at starte med - kun 9 LOC til den grundlæggende applikation. Martini er blevet kritiseret af Golang-samfundet, men har stadig et af de højest bedømte Github-projekter af enhver Golang-webramme. Forfatteren af Martini reagerede direkte på kritikken her. Nogle andre rammer inkluderer Revel, Gin og endda det indbyggede net/http-bibliotek.
package main
import "github.com/go-martini/martini"
func main() {
m := martini.Classic()
m.Get("/", func() string {
return "Hello world!"
})
m.Run()
}
Lad os bygge en app med det grundlæggende af vejen!
Tjenestebeskrivelse
Den oprettede tjeneste giver en meget grundlæggende blogapplikation. Følgende ruter er konstrueret:
GET /
:Returner bloggen (ved hjælp af en skabelon til at gengive).GET /json
:Returner blogindholdet i JSON-format.POST /new
:Tilføj et nyt indlæg (titel, resumé, indhold) til bloggen.
Den eksterne grænseflade til blogtjenesten er nøjagtig den samme for hvert sprog. For nemheds skyld vil MongoDB blive brugt som datalager for dette eksempel, da det er det enkleste at konfigurere, og vi behøver slet ikke bekymre os om skemaer. I en normal "blog-lignende" applikation vil en relationsdatabase sandsynligvis være nødvendig.
Tilføj et indlæg
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
Se HTML
GET /
Se JSON
GET /json
[
{
content:"Lorem ipsum dolor sit amet, consectetur ...",
title:"Test Post 1",
_id:{
$oid:"558329927315660001550970"
},
summary:"The First Test Post"
}
]
Applikationsstruktur
Hver applikation kan opdeles i følgende komponenter:
Opsætning af applikation
- Initialiser en applikation
- Kør applikationen
Anmodning
- Definer ruter, som en bruger kan anmode om data på (GET)
- Definer ruter, som en bruger kan indsende data på (POST)
Svar
- Gengiv JSON (
GET /json
) - Gengiv en skabelon (
GET /
)
Database
- Initialiser en forbindelse
- Indsæt data
- Hent data
Applikationsimplementering
- Docker!
Resten af denne artikel vil sammenligne hver af disse komponenter for hvert bibliotek. Formålet er ikke at antyde, at det ene af disse biblioteker er bedre end det andet - det er at give en specifik sammenligning mellem de tre værktøjer:
- Kolbe (Python)
- Sinatra (Ruby)
- Martini (Golang)
Projektopsætning
Alle projekter er bootstrapped ved hjælp af docker og docker-compose. Inden vi dykker ned i, hvordan hver applikation er bootstrapped under emhætten, kan vi bare bruge docker til at få hver enkelt op at køre på nøjagtig samme måde - docker-compose up
Seriøst, det er det! For hver applikation er der nu en Dockerfile
og en docker-compose.yml
fil, der angiver, hvad der sker, når du kører ovenstående kommando.
Python (kolbe) - Dockerfile
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
Denne Dockerfile
siger, at vi starter fra et basisbillede med Python 3.4 installeret, og tilføjer vores applikation til /app
bibliotek og bruge pip til at installere vores applikationskrav specificeret i requirements.txt
.
Ruby (sinatra)
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
Denne Dockerfile
siger, at vi starter fra et basisbillede med Ruby 2.2 installeret, og tilføjer vores applikation til /app
bibliotek og bruge bundler til at installere vores applikationskrav specificeret i 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
Denne Dockerfile
siger, at vi starter fra et basisbillede med Golang 1.3 installeret, og tilføjer vores applikation til /go/src/github.com/kpurdon/go-blog
bibliotek og få alle vores nødvendige afhængigheder ved hjælp af go get
kommando.
Initialiser/kør en applikation
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
Ruby (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
Definer en rute (GET/POST)
Python (kolbe)
# get
@app.route('/') # the default is GET only
def blog():
# ...
#post
@app.route('/new', methods=['POST'])
def new():
# ...
Ruby (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) {
// ...
}
Gengiv et JSON-svar
Python (kolbe)
Flask giver en jsonify()-metode, men da tjenesten bruger MongoDB, bruges mongodb bson-værktøjet.
from bson.json_util import dumps
return dumps(posts) # posts is a list of dicts [{}, {}]
Ruby (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
Gengiv et HTML-svar (skabelon)
Python (kolbe)
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>
Ruby (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>
Databaseforbindelse
Alle applikationerne bruger den mongodb-driver, der er specifik for sproget. Miljøvariablen DB_PORT_27017_TCP_ADDR
er IP-adressen for en linket docker-container (databasens ip).
Python (kolbe)
from pymongo import MongoClient
client = MongoClient(os.environ['DB_PORT_27017_TCP_ADDR'], 27017)
db = client.blog
Ruby (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()
Indsæt data fra en POST
Python (kolbe)
from flask import request
post = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
db.blog.insert_one(post)
Ruby (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
Hent data
Python (kolbe)
posts = db.blog.find()
Ruby (Sinatra)
@posts = client[:posts].find.to_a
Golang (Martini)
var posts []Post
db.C("posts").Find(nil).All(&posts)
Applikationsimplementering (Docker!)
En god løsning til at implementere alle disse applikationer er at bruge docker og docker-compose.
Python (kolbe)
Dockerfil
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
Ruby (Sinatra)
Dockerfil
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)
Dockerfil
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
Konklusion
Lad os afslutningsvis tage et kig på, hvad jeg mener er nogle få kategorier, hvor de præsenterede biblioteker adskiller sig fra hinanden.
Enkelhed
Mens Flask er meget let og læser klart, er Sinatra-appen den enkleste af de tre ved 23 LOC (sammenlignet med 46 for Flask og 42 for Martini). Af disse grunde er Sinatra vinderen i denne kategori. Det skal dog bemærkes, at Sinatras enkelhed skyldes mere standard "magi" - f.eks. implicit arbejde, der sker bag kulisserne. For nye brugere kan dette ofte føre til forvirring.
Her er et specifikt eksempel på "magi" i Sinatra:
params # the "request.form" logic in python is done "magically" behind the scenes in Sinatra.
Og den tilsvarende Flask-kode:
from flask import request
params = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
For begyndere at programmere er Flask og Sinatra bestemt enklere, men for en erfaren programmør med tid brugt på andre statisk indtastede sprog giver Martini en ret forenklet grænseflade.
Dokumentation
Flask-dokumentationen var den nemmeste at søge og mest tilgængelig. Mens Sinatra og Martini begge er veldokumenterede, var selve dokumentationen ikke så tilgængelig. Af denne grund er Flask vinderen i denne kategori.
Fællesskab
Flask er vinderen uden tvivl i denne kategori. Ruby-samfundet er oftere dogmatisk over for Rails som det eneste gode valg, hvis du har brug for mere end en grundlæggende service (selvom Padrino tilbyder dette oven i Sinatra). Golang-fællesskabet er stadig ikke i nærheden af en konsensus om en (eller endda nogle få) web-frameworks, hvilket kan forventes, da sproget i sig selv er så ungt. Python har imidlertid omfavnet en række tilgange til webudvikling, herunder Django til out-of-the-box fuldfunktionelle webapplikationer og Flask, Bottle, CheryPy og Tornado til en mikrorammetilgang.
Endelig afgørelse
Bemærk, at pointen med denne artikel ikke var at promovere et enkelt værktøj, snarere at give en upartisk sammenligning af Flask, Sinatra og Martini. Når det er sagt, ville jeg vælge Flask (Python) eller Sinatra (Ruby). Hvis du kommer fra et sprog som C eller Java, kan den statisk-typede natur af Golang måske appellere til dig. Hvis du er nybegynder, kan Flask være det bedste valg, da det er meget nemt at komme i gang, og der er meget lidt standard "magi". Min anbefaling er, at du er fleksibel i dine beslutninger, når du vælger et bibliotek til dit projekt.
Spørgsmål? Feedback? Kommenter venligst nedenfor. Tak!
Fortæl os også, hvis du er interesseret i at se nogle benchmarks.