Halo, Habr! Saya mempersembahkan kepada Anda terjemahan artikel "Bagaimana membangun aplikasi web pertama Anda dengan Go" oleh Ayooluwa Isaiah.
Ini adalah panduan untuk aplikasi web Go pertama Anda. Kami akan membuat aplikasi berita yang menggunakan API Berita untuk menerima artikel berita tentang topik tertentu, dan menyebarkannya ke server produksi pada akhirnya.
Anda dapat menemukan kode lengkap yang digunakan untuk tutorial ini di repositori GitHub ini.
Persyaratan
Satu-satunya persyaratan untuk tugas ini adalah Go diinstal pada komputer Anda dan Anda sedikit terbiasa dengan sintaks dan konstruksinya. Versi Go yang saya gunakan untuk membuat aplikasi ini juga yang terbaru pada saat penulisan: 1.12.9 . Untuk melihat versi Go yang diinstal, gunakan perintah go version
.
Jika Anda merasa tugas ini terlalu sulit untuk Anda, buka pelajaran bahasa pengantar saya sebelumnya, yang akan membantu Anda memulai.
Jadi mari kita mulai!
Kami mengkloning repositori file mulai pada GitHub dan cd
ke direktori yang dibuat. Kami memiliki tiga file utama: Di file main.go
kami akan menulis semua kode Go untuk tugas ini. File index.html
adalah template yang akan dikirim ke browser, dan
untuk aplikasi berada di assets/styles.css
.
Buat server web dasar
Mari kita mulai dengan membuat server inti yang mengirimkan teks "Hello World!" Ke browser saat menjalankan permintaan GET ke root server. Ubah file main.go
Anda menjadi seperti ini:
package main import ( "net/http" "os" ) func indexHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("<h1>Hello World!</h1>")) } func main() { port := os.Getenv("PORT") if port == "" { port = "3000" } mux := http.NewServeMux() mux.HandleFunc("/", indexHandler) http.ListenAndServe(":"+port, mux) }
Baris pertama package main
- menyatakan bahwa kode dalam file main.go
dalam paket utama. Setelah itu, kami mengimpor paket net/http
, yang menyediakan implementasi klien HTTP dan server untuk digunakan dalam aplikasi kami. Paket ini adalah bagian dari perpustakaan standar dan disertakan dengan setiap instalasi Go.
Dalam fungsi main
, http.NewServeMux()
membuat multiplexer permintaan HTTP baru dan menetapkannya ke variabel mux
. Pada dasarnya, multiplexer permintaan cocok dengan URL permintaan yang masuk ke daftar jalur terdaftar dan memanggil penangan yang sesuai untuk jalur tersebut setiap kali ditemukan kecocokan.
Selanjutnya, kami mendaftarkan fungsi pengendali pertama kami untuk jalur root /
. Fungsi handler ini adalah argumen kedua untuk HandleFunc
dan selalu memiliki func (w http.ResponseWriter, r * http.Request)
tanda tangan func (w http.ResponseWriter, r * http.Request)
.
Jika Anda melihat fungsi indexHandler
, Anda akan melihat bahwa itu hanya memiliki tanda tangan seperti itu, yang membuatnya menjadi argumen kedua yang valid untuk HandleFunc
. Parameter w
adalah struktur yang kami gunakan untuk mengirim respons ke permintaan HTTP. Ini mengimplementasikan metode Write()
, yang mengambil irisan byte dan menulis data gabungan sebagai bagian dari respons HTTP.
Di sisi lain, parameter r
mewakili permintaan HTTP yang diterima dari klien. Ini adalah cara kami mengakses data yang dikirim oleh browser web di server. Kami belum menggunakannya di sini, tetapi kami pasti akan menggunakannya nanti.
Akhirnya, kita memiliki metode http.ListenAndServe()
yang memulai server pada port 3000 jika port tidak diatur oleh lingkungan. Jangan ragu untuk menggunakan port yang berbeda jika 3000 digunakan di komputer Anda.
Kemudian kompilasi dan jalankan kode yang baru saja Anda tulis:
go run main.go
Jika Anda membuka http: // localhost: 3000 di browser Anda, Anda akan melihat teks "Hello World!".

Pergi Templat
Mari kita lihat dasar-dasar templating di Go. Jika Anda terbiasa dengan template dalam bahasa lain, ini seharusnya cukup mudah untuk dipahami.
Template menyediakan cara mudah untuk menyesuaikan output aplikasi web Anda tergantung pada rute tanpa harus menulis kode yang sama di tempat yang berbeda. Misalnya, kita dapat membuat templat untuk bilah navigasi dan menggunakannya di semua halaman situs tanpa menggandakan kode. Selain itu, kami juga mendapat kesempatan untuk menambahkan beberapa logika dasar ke halaman web kami.
Go menyediakan dua pustaka templat di pustaka standar: text/template
dan html/template
. Keduanya menyediakan antarmuka yang sama, namun paket html/template
digunakan untuk menghasilkan output HTML yang dilindungi terhadap injeksi kode, jadi kami akan menggunakannya di sini.
Impor paket ini ke file main.go
Anda dan gunakan sebagai berikut:
package main import ( "html/template" "net/http" "os" ) var tpl = template.Must(template.ParseFiles("index.html")) func indexHandler(w http.ResponseWriter, r *http.Request) { tpl.Execute(w, nil) } func main() { port := os.Getenv("PORT") if port == "" { port = "3000" } mux := http.NewServeMux() mux.HandleFunc("/", indexHandler) http.ListenAndServe(":"+port, mux) }
tpl
adalah variabel tingkat paket yang menunjukkan definisi templat dari file yang disediakan. Panggilan template.ParseFiles
mem-parsing file index.html
di root direktori proyek kami dan memeriksa validitasnya.
Kami membungkus panggilan template.ParseFiles
di template.Must
sehingga kode menyebabkan kepanikan ketika kesalahan terjadi. Alasan kami panik di sini daripada mencoba menangani kesalahan adalah karena tidak masuk akal untuk melanjutkan mengeksekusi kode jika kami memiliki templat yang tidak valid. Ini adalah masalah yang perlu diperbaiki sebelum mencoba me-restart server.
Dalam fungsi indexHandler
kami menjalankan template yang dibuat sebelumnya dengan memberikan dua argumen: di mana kami ingin menulis output dan data yang ingin kami sampaikan ke template.
Dalam kasus di atas, kami menulis output ke antarmuka ResponseWriter
dan, karena kami tidak memiliki data untuk dikirimkan ke templat kami saat ini, nil
dilewatkan sebagai argumen kedua.
Hentikan proses yang berjalan di terminal Anda menggunakan Ctrl-C dan mulai lagi dengan go run main.go
, lalu segarkan browser Anda. Anda akan melihat teks "Demo Aplikasi Berita" pada halaman seperti yang ditunjukkan di bawah ini:

Tambahkan bilah navigasi ke halaman
Ganti konten <body>
dalam file index.html Anda seperti yang ditunjukkan di bawah ini:
<main> <header> <a class="logo" href="/">News Demo</a> <form action="/search" method="GET"> <input autofocus class="search-input" value="" placeholder="Enter a news topic" type="search" name="q"> </form> <a href="https://github.com/freshman-tech/news" class="button github-button">View on Github</a> </header> </main>
Kemudian reboot server dan segarkan browser Anda. Anda harus melihat sesuatu yang mirip dengan ini:

Bekerja dengan file statis
Harap perhatikan bahwa bilah navigasi yang kami tambahkan di atas tidak memiliki gaya, walaupun faktanya kami telah menentukannya di <head>
dokumen kami.
Ini karena jalur /
sebenarnya cocok dengan semua jalur yang tidak diproses di tempat lain. Oleh karena itu, jika Anda membuka http: // localhost: 3000 / aset / style.css , Anda masih akan mendapatkan beranda Demo Berita alih-alih file CSS karena rute /assets/style.css
belum dideklarasikan secara spesifik.
Tetapi kebutuhan untuk mendeklarasikan penangan eksplisit untuk semua file statis kami tidak realistis dan tidak dapat diukur. Untungnya, kita dapat membuat satu penangan untuk melayani semua sumber daya statis.
Hal pertama yang harus dilakukan adalah membuat instance dari objek server file, melewati direktori di mana semua file statis kita berada:
fs := http.FileServer(http.Dir("assets"))
Selanjutnya, kita perlu memberi tahu router kita untuk menggunakan objek server file ini untuk semua jalur yang dimulai dengan /assets/
prefix:
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
Sekarang semuanya:
Nyalakan ulang server dan segarkan peramban. Gaya harus dihidupkan seperti yang ditunjukkan di bawah ini:

Kami membuat rute / pencarian
Mari kita buat rute yang menangani permintaan pencarian untuk artikel berita. Kami akan menggunakan API Berita untuk memproses permintaan, jadi Anda harus mendaftar untuk menerima kunci API gratis di sini .
Rute ini mengharapkan dua parameter kueri: q
mewakili kueri pengguna, dan page
digunakan untuk menggulir hasil. Parameter page
ini adalah opsional. Jika tidak termasuk dalam URL, kami hanya berasumsi bahwa nomor halaman hasil diatur ke "1".
Tambahkan handler berikut di bawah indexHandler
ke file main.go
Anda:
func searchHandler(w http.ResponseWriter, r *http.Request) { u, err := url.Parse(r.URL.String()) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Internal server error")) return } params := u.Query() searchKey := params.Get("q") page := params.Get("page") if page == "" { page = "1" } fmt.Println("Search Query is: ", searchKey) fmt.Println("Results page is: ", page) }
Kode di atas mengekstrak parameter q
dan page
dari URL permintaan dan menampilkan keduanya di terminal.
Kemudian daftarkan fungsi searchHandler
sebagai penangan path /search
, seperti yang ditunjukkan di bawah ini:
func main() { port := os.Getenv("PORT") if port == "" { port = "3000" } mux := http.NewServeMux() fs := http.FileServer(http.Dir("assets")) mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
Ingatlah untuk mengimpor paket fmt
dan net/url
dari atas:
import ( "fmt" "html/template" "net/http" "net/url" "os" )
Sekarang restart server, masukkan permintaan di bidang pencarian dan periksa terminal. Anda harus melihat permintaan Anda di terminal, seperti yang ditunjukkan di bawah ini:
Buat model data
Saat kami mengajukan permintaan ke News API/everything
titik akhir News API/everything
, kami mengharapkan respons json dalam format berikut:
{ "status": "ok", "totalResults": 4661, "articles": [ { "source": { "id": null, "name": "Gizmodo.com" }, "author": "Jennings Brown", "title": "World's Dumbest Bitcoin Scammer Tries to Scam Bitcoin Educator, Gets Scammed in The Process", "description": "Ben Perrin is a Canadian cryptocurrency enthusiast and educator who hosts a bitcoin show on YouTube. This is immediately apparent after a quick a look at all his social media. Ten seconds of viewing on of his videos will show that he is knowledgeable about di…", "url": "https://gizmodo.com/worlds-dumbest-bitcoin-scammer-tries-to-scam-bitcoin-ed-1837032058", "urlToImage": "https://i.kinja-img.com/gawker-media/image/upload/s--uLIW_Oxp--/c_fill,fl_progressive,g_center,h_900,q_80,w_1600/s4us4gembzxlsjrkmnbi.png", "publishedAt": "2019-08-07T16:30:00Z", "content": "Ben Perrin is a Canadian cryptocurrency enthusiast and educator who hosts a bitcoin show on YouTube. This is immediately apparent after a quick a look at all his social media. Ten seconds of viewing on of his videos will show that he is knowledgeable about..." } ] }
Untuk bekerja dengan data ini di Go, kita perlu membuat struktur yang mencerminkan data saat mendekode badan respons. Tentu saja, Anda dapat melakukannya secara manual, tetapi saya lebih suka menggunakan situs web JSON-to-Go , yang membuat proses ini sangat mudah. Ini menghasilkan struktur Go (dengan tag) yang akan berfungsi untuk JSON ini.
Yang harus Anda lakukan adalah menyalin objek JSON dan menempelkannya ke bidang yang ditandai dengan JSON , lalu menyalin hasilnya dan menempelkannya ke kode Anda. Inilah yang kita dapatkan untuk objek JSON di atas:
type AutoGenerated struct { Status string `json:"status"` TotalResults int `json:"totalResults"` Articles []struct { Source struct { ID interface{} `json:"id"` Name string `json:"name"` } `json:"source"` Author string `json:"author"` Title string `json:"title"` Description string `json:"description"` URL string `json:"url"` URLToImage string `json:"urlToImage"` PublishedAt time.Time `json:"publishedAt"` Content string `json:"content"` } `json:"articles"` }

Saya membuat beberapa perubahan pada struktur AutoGenerated
dengan memisahkan fragmen Articles
ke dalam strukturnya sendiri dan memperbarui nama struktur. Rekatkan deklarasi variabel tpl
berikut ke main.go
dan tambahkan paket time
ke impor Anda:
type Source struct { ID interface{} `json:"id"` Name string `json:"name"` } type Article struct { Source Source `json:"source"` Author string `json:"author"` Title string `json:"title"` Description string `json:"description"` URL string `json:"url"` URLToImage string `json:"urlToImage"` PublishedAt time.Time `json:"publishedAt"` Content string `json:"content"` } type Results struct { Status string `json:"status"` TotalResults int `json:"totalResults"` Articles []Article `json:"articles"` }
Seperti yang Anda ketahui, Go mengharuskan semua bidang yang diekspor dalam struktur dimulai dengan huruf kapital. Namun, merupakan kebiasaan untuk mewakili bidang JSON menggunakan camelCase atau snake_case , yang tidak dimulai dengan huruf kapital.
Oleh karena itu, kami menggunakan tag bidang struktur seperti json:"id"
untuk secara eksplisit menampilkan bidang struktur di bidang JSON, seperti yang ditunjukkan di atas. Ini juga memungkinkan Anda untuk menggunakan nama yang sama sekali berbeda untuk bidang struktur dan bidang json yang sesuai, jika perlu.
Terakhir, mari kita buat jenis struktur yang berbeda untuk setiap permintaan pencarian. Tambahkan ini di bawah struktur Results
di main.go
:
type Search struct { SearchKey string NextPage int TotalPages int Results Results }
Struktur ini mewakili setiap permintaan pencarian yang dibuat oleh pengguna. SearchKey
adalah kueri itu sendiri, bidang NextPage
memungkinkan NextPage
untuk menggulir hasil, TotalPages
- jumlah total halaman hasil permintaan, dan Results
- halaman saat ini dari hasil permintaan.
Kirim permintaan menggunakan API Berita dan berikan hasilnya
Sekarang kami memiliki model data untuk aplikasi kami, mari lanjutkan dan buat permintaan ke News API, dan kemudian berikan hasilnya di halaman.
Karena API Berita memerlukan kunci API, kita perlu menemukan cara untuk meneruskannya dalam aplikasi kita tanpa kode yang sulit dalam kode. Variabel lingkungan adalah pendekatan yang umum, tetapi saya memutuskan untuk menggunakan flag baris perintah sebagai gantinya. Go menyediakan paket flag
yang mendukung analisis dasar bendera baris perintah, dan inilah yang akan kami gunakan di sini.
Pertama mendeklarasikan variabel apiKey
baru di bawah variabel tpl
:
var apiKey *string
Kemudian gunakan dalam fungsi main
sebagai berikut:
func main() { apiKey = flag.String("apikey", "", "Newsapi.org access key") flag.Parse() if *apiKey == "" { log.Fatal("apiKey must be set") }
Di sini kita memanggil metode flag.String()
, yang memungkinkan kita untuk mendefinisikan flag string. Argumen pertama untuk metode ini adalah nama bendera, yang kedua adalah nilai default, dan yang ketiga adalah deskripsi penggunaan.
Setelah mendefinisikan semua flag, Anda perlu memanggil flag.Parse()
untuk benar-benar flag.Parse()
. Akhirnya, karena apikey
adalah komponen yang diperlukan untuk aplikasi ini, kami memastikan bahwa program mogok jika flag ini tidak diatur selama eksekusi program.
Pastikan Anda menambahkan paket flag
ke impor Anda, kemudian restart server dan berikan flag apikey
diperlukan, seperti yang ditunjukkan di bawah ini:
go run main.go -apikey=<your newsapi access key>
Selanjutnya, mari kita lanjutkan dan perbarui searchHandler
sehingga permintaan pencarian pengguna dikirim ke newsapi.org dan hasilnya ditampilkan di templat kami.
Ganti dua panggilan ke metode fmt.Println()
di akhir fungsi searchHandler
kode berikut:
func searchHandler(w http.ResponseWriter, r *http.Request) {
Pertama, kami membuat instance baru dari struktur Search
dan mengatur nilai bidang SearchKey
ke nilai parameter URL q
dalam permintaan HTTP.
Setelah itu, kami mengonversi variabel page
menjadi integer dan menetapkan hasilnya ke bidang NextPage
variabel search
. Kemudian kita membuat variabel pageSize
dan menetapkan nilainya menjadi 20. Variabel pageSize
ini menunjukkan jumlah hasil yang akan dikembalikan oleh API berita dalam responsnya. Nilai ini dapat berkisar dari 0 hingga 100.
Lalu kami membuat titik akhir menggunakan fmt.Sprintf()
dan membuat permintaan GET untuk itu. Jika respons dari News API tidak 200 OK , kami akan mengembalikan kesalahan server umum kepada klien. Kalau tidak, badan respons diuraikan dalam search.Results
. search.Results
.
Lalu kami menghitung jumlah total halaman dengan membagi bidang TotalResults
dengan TotalResults
. Misalnya, jika kueri mengembalikan 100 hasil, dan kami hanya melihat 20 pada satu waktu, kami perlu menelusuri lima halaman untuk melihat semua 100 hasil untuk kueri itu.
Setelah itu, kami merender template kami dan meneruskan variabel search
sebagai antarmuka data. Ini memungkinkan kami untuk mengakses data dari objek JSON di templat kami, seperti yang akan Anda lihat.
Sebelum beralih ke index.html
, pastikan untuk memperbarui impor Anda seperti yang ditunjukkan di bawah ini:
import ( "encoding/json" "flag" "fmt" "html/template" "log" "math" "net/http" "net/url" "os" "strconv" "time" )
Mari kita lanjutkan dan tampilkan hasilnya pada halaman dengan mengubah file index.html
sebagai berikut. Tambahkan ini di bawah <header>
:
<section class="container"> <ul class="search-results"> {{ range .Results.Articles }} <li class="news-article"> <div> <a target="_blank" rel="noreferrer noopener" href="{{.URL}}"> <h3 class="title">{{.Title }}</h3> </a> <p class="description">{{ .Description }}</p> <div class="metadata"> <p class="source">{{ .Source.Name }}</p> <time class="published-date">{{ .PublishedAt }}</time> </div> </div> <img class="article-image" src="{{ .URLToImage }}"> </li> {{ end }} </ul> </section>
Untuk mengakses bidang struktur dalam templat, kami menggunakan operator titik. Operator ini merujuk ke objek struktur (dalam hal ini, search
), dan kemudian di dalam templat kami cukup menentukan nama bidang (sebagai {{.Results}}
).
Blok range
memungkinkan kita untuk beralih di atas slice di Go dan output beberapa HTML untuk setiap elemen di slice. Di sini, kami mengulangi potongan struktur Article
yang terkandung dalam bidang Articles
dan menampilkan HTML di setiap iterasi.
Mulai ulang server, segarkan peramban, dan cari berita tentang topik populer. Anda harus mendapatkan daftar 20 hasil per halaman, seperti yang ditunjukkan pada gambar di bawah.

Perhatikan bahwa permintaan pencarian menghilang dari input ketika halaman disegarkan dengan hasilnya. Idealnya, kueri harus disimpan sampai pengguna melakukan pencarian baru. Inilah cara kerja Google Penelusuran, misalnya.
Kita dapat dengan mudah memperbaikinya dengan memperbarui atribut value
dari tag input
dalam file index.html
kami sebagai berikut:
<input autofocus class="search-input" value="{{ .SearchKey }}" placeholder="Enter a news topic" type="search" name="q">
Mulai ulang browser Anda dan lakukan pencarian baru. Permintaan pencarian akan disimpan seperti yang ditunjukkan di bawah ini:

Jika Anda melihat tanggal di setiap artikel, Anda akan melihat bahwa itu tidak dapat dibaca. Output saat ini menunjukkan bagaimana API Berita mengembalikan tanggal publikasi artikel. Tetapi kita dapat dengan mudah mengubah ini dengan menambahkan metode ke struktur Article
dan menggunakannya untuk memformat tanggal alih-alih menggunakan nilai default.
Mari kita tambahkan kode berikut tepat di bawah struktur Article
di main.go
:
func (a *Article) FormatPublishedDate() string { year, month, day := a.PublishedAt.Date() return fmt.Sprintf("%v %d, %d", month, day, year) }
Di sini, metode FormatPublishedDate
baru dibuat dalam struktur Article
, dan metode ini memformat bidang PublishedAt
di Article
dan mengembalikan string dalam format berikut: 10 2009
.
Untuk menggunakan metode baru ini di templat Anda, ganti .PublishedAt
dengan .FormatPublishedDate
di file index.html
Anda. Kemudian restart server dan ulangi permintaan pencarian sebelumnya. Ini akan menampilkan hasil dengan waktu yang diformat dengan benar, seperti yang ditunjukkan di bawah ini:

Tampilkan jumlah total hasil.
Mari kita tingkatkan antarmuka pengguna aplikasi berita kami dengan menunjukkan jumlah total hasil di bagian atas halaman, dan kemudian menampilkan pesan jika tidak ada hasil yang ditemukan untuk permintaan tertentu.
Yang harus Anda lakukan adalah menambahkan kode berikut sebagai anak dari .container
, tepat di atas elemen .search-results
dalam file index.html
Anda:
<div class="result-count"> {{ if (gt .Results.TotalResults 0)}} <p>About <strong>{{ .Results.TotalResults }}</strong> results were found.</p> {{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }} <p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p> {{ end }} </div>
Go , . gt
, , TotalResults
Results
. , .
, SearchKey
( (ne .SearchKey "")
) TotalResults
( (eq .Results.TotalResults 0)
), «No results found».
, . «No results found».

. , :

20 , , .
Next , . , , Search
main.go
:
func (s *Search) IsLastPage() bool { return s.NextPage >= s.TotalPages }
, NextPage
, TotalPages
Search
. , NextPage
, . :
func searchHandler(w http.ResponseWriter, r *http.Request) {
, , . .search-results
index.html
.
<div class="pagination"> {{ if (ne .IsLastPage true) }} <a href="/search?q={{ .SearchKey }}&page={{ .NextPage }}" class="button next-page">Next</a> {{ end }} </div>
, Next .
, href
/search
q
, NextPage
page
.
Previous . , 1. , CurrentPage()
Search
, . IsLastPage
:
func (s *Search) CurrentPage() int { if s.NextPage == 1 { return s.NextPage } return s.NextPage - 1 }
NextPage - 1
, , NextPage
1. , 1 . :
func (s *Search) PreviousPage() int { return s.CurrentPage() - 1 }
, Previous , 1. .pagination
index.html
:
<div class="pagination"> {{ if (gt .NextPage 2) }} <a href="/search?q={{ .SearchKey }}&page={{ .PreviousPage }}" class="button previous-page">Previous</a> {{ end }} {{ if (ne .IsLastPage true) }} <a href="/search?q={{ .SearchKey }}&page={{ .NextPage }}" class="button next-page">Next</a> {{ end }} </div>
. , :
, , , , .
index.html
:
<div class="result-count"> {{ if (gt .Results.TotalResults 0)}} <p>About <strong>{{ .Results.TotalResults }}</strong> results were found. You are on page <strong>{{ .CurrentPage }}</strong> of <strong> {{ .TotalPages }}</strong>.</p> {{ else if (ne .SearchKey "") and (eq .Results.TotalResults 0) }} <p>No results found for your query: <strong>{{ .SearchKey }}</strong>.</p> {{ end }} </div>
, , .

Heroku
, , Heroku. , , . . freshman-news .
, Heroku . heroku login
, Heroku.
, git- . , git init
, , heroku git-. freshman-news
.
heroku git:remote -a freshman-news
Procfile ( touch Procfile
) :
web: bin/news-demo -apikey $NEWS_API_KEY
GitHub Go, , go.mod
, . , , .
module github.com/freshman-tech/news-demo go 1.12.9
Settings Heroku Reveal Config Vars . NEWS_API_KEY , .

, Heroku :
git add . git commit -m "Initial commit" git push heroku master
https://__.herokuapp.com , .
Kesimpulan
News Go -. , Heroku.
, . - , , .
Terima kasih sudah membaca!