Na een recente vergelijking van Python, Ruby en Golang voor een opdrachtregeltoepassing, besloot ik hetzelfde patroon te gebruiken om het bouwen van een eenvoudige webservice te vergelijken. Ik heb Flask (Python), Sinatra (Ruby) en Martini (Golang) geselecteerd voor deze vergelijking. Ja, er zijn veel andere opties voor webtoepassingsbibliotheken in elke taal, maar ik vond dat deze drie goed geschikt waren voor vergelijking.
Bibliotheekoverzichten
Hier is een vergelijking op hoog niveau van de bibliotheken door Stackshare.
Koel (Python)
Flask is een micro-framework voor Python gebaseerd op Werkzeug, Jinja2 en goede bedoelingen.
Voor zeer eenvoudige toepassingen, zoals degene die in deze demo wordt getoond, is Flask een goede keuze. De basistoepassing van Flask is slechts 7 regels code (LOC) in een enkel Python-bronbestand. Het voordeel van Flask ten opzichte van andere Python-webbibliotheken (zoals Django of Pyramid) is dat je klein kunt beginnen en zo nodig kunt opbouwen naar een complexere applicatie.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
Sinatra (Ruby)
Sinatra is een DSL voor het snel creëren van webapplicaties in Ruby met minimale inspanning.
Net als Flask is Sinatra geweldig voor eenvoudige toepassingen. De basistoepassing van Sinatra is slechts 4 LOC in een enkel Ruby-bronbestand. Sinatra wordt om dezelfde reden als Flask gebruikt in plaats van bibliotheken zoals Ruby on Rails - u kunt klein beginnen en de toepassing naar behoefte uitbreiden.
require 'sinatra'
get '/hi' do
"Hello World!"
end
Martini (Golang)
Martini is een krachtig pakket om snel modulaire webapplicaties/diensten in Golang te schrijven.
Martini wordt geleverd met een paar extra batterijen dan zowel Sinatra als Flask, maar is nog steeds erg licht om mee te beginnen - slechts 9 LOC voor de basistoepassing. Martini heeft enige kritiek gekregen van de Golang-gemeenschap, maar heeft nog steeds een van de best beoordeelde Github-projecten van elk Golang-webframework. De auteur van Martini reageerde hier direct op de kritiek. Enkele andere frameworks omvatten Revel, Gin en zelfs de ingebouwde net/http-bibliotheek.
package main
import "github.com/go-martini/martini"
func main() {
m := martini.Classic()
m.Get("/", func() string {
return "Hello world!"
})
m.Run()
}
Laten we, met de basis uit de weg, een app bouwen!
Servicebeschrijving
De gecreëerde service biedt een zeer eenvoudige blogtoepassing. De volgende routes zijn aangelegd:
GET /
:de blog retourneren (met behulp van een sjabloon om weer te geven).GET /json
:retourneer de bloginhoud in JSON-indeling.POST /new
:voeg een nieuw bericht (titel, samenvatting, inhoud) toe aan de blog.
De externe interface naar de blogservice is voor elke taal precies hetzelfde. Voor de eenvoud zal MongoDB worden gebruikt als de gegevensopslag voor dit voorbeeld, omdat dit de eenvoudigste is om in te stellen en we ons helemaal geen zorgen hoeven te maken over schema's. In een normale "blog-achtige" applicatie zou waarschijnlijk een relationele database nodig zijn.
Een bericht toevoegen
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
Bekijk de HTML
GET /
Bekijk de JSON
GET /json
[
{
content:"Lorem ipsum dolor sit amet, consectetur ...",
title:"Test Post 1",
_id:{
$oid:"558329927315660001550970"
},
summary:"The First Test Post"
}
]
Applicatiestructuur
Elke applicatie kan worden onderverdeeld in de volgende componenten:
Toepassing instellen
- Een toepassing initialiseren
- Voer de applicatie uit
Verzoek
- Definieer routes waarop een gebruiker gegevens kan opvragen (GET)
- Definieer routes waarop een gebruiker gegevens kan indienen (POST)
Reactie
- JSON renderen (
GET /json
) - Render een sjabloon (
GET /
)
Database
- Een verbinding initialiseren
- Gegevens invoegen
- Gegevens ophalen
App-implementatie
- Dokter!
De rest van dit artikel vergelijkt elk van deze componenten voor elke bibliotheek. Het doel is niet om te suggereren dat een van deze bibliotheken beter is dan de andere - het is om een specifieke vergelijking te geven tussen de drie tools:
- Kos (Python)
- Sinatra (Ruby)
- Martini (Golang)
Projectconfiguratie
Alle projecten worden opgestart met behulp van docker en docker-compose. Voordat we ingaan op hoe elke applicatie onder de motorkap wordt opgestart, kunnen we gewoon Docker gebruiken om ze allemaal op precies dezelfde manier aan de gang te krijgen - docker-compose up
Serieus, dat is het! Nu is er voor elke applicatie een Dockerfile
en een docker-compose.yml
bestand dat specificeert wat er gebeurt als je de bovenstaande opdracht uitvoert.
Python (kolf) - Dockerfile
FROM python:3.4
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt
Dit Dockerfile
zegt dat we beginnen met een basisafbeelding waarop Python 3.4 is geïnstalleerd en onze applicatie toevoegen aan de /app
directory en het gebruik van pip om onze applicatievereisten te installeren die zijn gespecificeerd in requirements.txt
.
Ruby (sinatra)
FROM ruby:2.2
ADD . /app
WORKDIR /app
RUN bundle install
Dit Dockerfile
zegt dat we beginnen met een basisimage waarop Ruby 2.2 is geïnstalleerd en onze applicatie toevoegen aan de /app
directory en het gebruik van bundelaar om onze applicatievereisten te installeren die zijn gespecificeerd in het 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
Dit Dockerfile
zegt dat we beginnen met een basisimage met Golang 1.3 geïnstalleerd, en onze applicatie toevoegen aan de /go/src/github.com/kpurdon/go-blog
directory en het verkrijgen van al onze noodzakelijke afhankelijkheden met behulp van de go get
commando.
Een toepassing initialiseren/uitvoeren
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
Definieer een route (GET/POST)
Python (Flask)
# 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) {
// ...
}
Een JSON-reactie weergeven
Python (Flask)
Flask biedt een jsonify()-methode, maar aangezien de service MongoDB gebruikt, wordt het hulpprogramma mongoDB bson gebruikt.
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
Een HTML-antwoord weergeven (template)
Python (Flask)
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>
Databaseverbinding
Alle toepassingen gebruiken het mongodb-stuurprogramma dat specifiek is voor de taal. De omgevingsvariabele DB_PORT_27017_TCP_ADDR
is het IP-adres van een gekoppelde docker-container (de database-ip).
Python (Flask)
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()
Gegevens uit een POST invoegen
Python (Flask)
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
Gegevens ophalen
Python (Flask)
posts = db.blog.find()
Ruby (Sinatra)
@posts = client[:posts].find.to_a
Golang (Martini)
var posts []Post
db.C("posts").Find(nil).All(&posts)
Applicatie-implementatie (Docker!)
Een geweldige oplossing voor het implementeren van al deze applicaties is het gebruik van docker en docker-compose.
Python (Flask)
Dockerbestand
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)
Dockerbestand
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)
Dockerbestand
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
Conclusie
Laten we tot slot eens kijken naar wat volgens mij een paar categorieën zijn waarin de gepresenteerde bibliotheken zich van elkaar scheiden.
Eenvoud
Hoewel Flask erg licht is en duidelijk leesbaar is, is de Sinatra-app de eenvoudigste van de drie met 23 LOC (vergeleken met 46 voor Flask en 42 voor Martini). Om deze redenen is Sinatra de winnaar in deze categorie. Er moet echter worden opgemerkt dat de eenvoud van Sinatra te wijten is aan meer standaard "magie" - bijvoorbeeld impliciet werk dat achter de schermen gebeurt. Voor nieuwe gebruikers kan dit vaak tot verwarring leiden.
Hier is een specifiek voorbeeld van "magie" in Sinatra:
params # the "request.form" logic in python is done "magically" behind the scenes in Sinatra.
En de equivalente Flask-code:
from flask import request
params = {
'title': request.form['title'],
'summary': request.form['summary'],
'content': request.form['content']
}
Voor beginners in het programmeren zijn Flask en Sinatra zeker eenvoudiger, maar voor een ervaren programmeur met tijd doorgebracht in andere statisch getypte talen biedt Martini een vrij simplistische interface.
Documentatie
De Flask-documentatie was het eenvoudigst te doorzoeken en het meest benaderbaar. Hoewel Sinatra en Martini beide goed gedocumenteerd zijn, was de documentatie zelf niet zo benaderbaar. Om deze reden is Flask de winnaar in deze categorie.
Gemeenschap
Flask is zonder twijfel de winnaar in deze categorie. De Ruby-gemeenschap is vaak dogmatisch dat Rails de enige goede keuze is als je meer nodig hebt dan een basisservice (hoewel Padrino dit bovenop Sinatra aanbiedt). De Golang-gemeenschap is nog lang niet in de buurt van een consensus over één (of zelfs enkele) webframeworks, wat te verwachten is omdat de taal zelf zo jong is. Python heeft echter een aantal benaderingen van webontwikkeling omarmd, waaronder Django voor kant-en-klare webapplicaties met volledige functionaliteit en Flask, Bottle, CheryPy en Tornado voor een microframework-benadering.
Definitieve bepaling
Merk op dat het punt van dit artikel niet was om een enkel hulpmiddel te promoten, maar om een onbevooroordeelde vergelijking te geven van Flask, Sinatra en Martini. Dat gezegd hebbende, zou ik Flask (Python) of Sinatra (Ruby) selecteren. Als je uit een taal als C of Java komt, kan het statisch getypeerde karakter van Golang je misschien aanspreken. Als je een beginner bent, is Flask misschien wel de beste keuze, omdat het heel gemakkelijk is om aan de slag te gaan en er is heel weinig "magie" standaard. Mijn aanbeveling is dat u flexibel bent in uw beslissingen bij het selecteren van een bibliotheek voor uw project.
Vragen? Feedback? Reageer hieronder. Bedankt!
Laat het ons ook weten als u geïnteresseerd bent in enkele benchmarks.