Как я 8 месяцев переписывал свою криптовалюту с PHP на Go. Часть 1

от автора

«Не звони и не пиши мне больше!!!!» — пришла смс-ка от моей девушки Кати. Через пару часов я осознал, что теперь у меня появилась куча свободного времени и я решил переписать Dcoin на Go.

Введение

4,5 года назад я имел неосторожность начать писать свою криптовалюту на совсем неподходящем для этого дела языке — на PHP. В итоге, конечно, написал (я упрямый), но получился костыль на костыле и то, что оно вообще работало было просто какой-то магией.
Сразу хочу предупредить, программер я самоучка-недоучка и пишу код, мягко сказать, неидеально.

Для тех, кому интересно, что там с Катей, я сделал спойлеры, ну а кому не интересно, просто не обращайте внимание на Катю.

Итог 8 месяцев: приложение работает на Win (64/32), OSX(64/32), Linux(64/32), FreeBSD(64/32), Android, IOS (будет круто, если кто-то закинет в App Store).
Общего кода ~73к строк, кода под разные ОС где-то несколько сотен строчек.
40к — обработка/генерация блоков/тр-ий, 17.5к — контроллеры для интерфейса, 15.5к — шаблоны
Поддерживаются PostgreSQL, SQLite, MySQL.

Тех, кто будет тестировать мое творение, предупреждаю — могут быть баги, и если у Вас есть время, чиркните о них, пожалуйста, на darwin@dcoin.club или в личку на хабре. Пожелания и советы тоже приветствуются.

Ну а теперь расскажу свою историю, как я всё это писал, чего нового узнал и пр. Надеюсь кто-то почерпнет что-то полезное для себя из моих статей.

Старт

Прошло несколько часов после смс-ки на Кати. Решение переписать Dcoin уже было принято. Нужно было с чего-то начать. Начал гуглить, как изучить Go с нуля. Где-то 2-3 дня штудировал http://golang-book.ru/ и https://tour.golang.org/, затем скачал вот эту книгу, на неё ушло где-то 2 недели, выполнял только примеры, самостоятельную работу пропускал. Было сложно, но интересно. Хотелось уже как можно скорее приступить к переписанию моих PHP-исходников.

Ничего нового я уже давно не изучал, мозг не привык к такому режиму. Купил в аптеке Омегу-3 и Ноотропил, у меня под ними лучше инфа запоминается, хотя может это и самовнушение.

Дочитал последнюю главу. Наконец можно было приступить к самому интересному.
Задача вполне понятная: переписать несколько десятков тысяч строк PHP-кода. Но с чего начать, совершенно не понятно. Решил начать с самого простого — с веб-сервера.

Про Катю

Расскажу сперва как всё началось. Познакомился я с ней в интернете на сайте знакомств в начале 2015-го. Влюбился почти сразу. Уж больно внешне она была в моем вкусе. Гуляли по парку, шутили, смеялись, прикалывались, стало холодно, расставаться не хотелось. Позвал к себе мультики смотреть. Сели в такси, приехали. Включил «Три богатыря: Ход конем», очень смешной мультик, лежали на кровати, смеялись до слез.

Revel

По описанию не очень понял, для чего он. Поставил, поэкспериментировал, стало ясно — точно не подходит. Revel для сайтов.

Beego

Работает на модулях. Есть неплохие доки на русском. Я взял 2 модуля config и session.

Gorilla

mux — хорошая штука, но применения для себя найти не смог.

В итоге остановился на чистом net/http + html/template

Веб-сервер

Свой веб-сервера в Go поднять очень просто. Вот пример файлового сервера:

package main import "net/http" func main() { 	changeHeaderThenServe := func(h http.Handler) http.HandlerFunc { 		return func(w http.ResponseWriter, r *http.Request) { 			h.ServeHTTP(w, r) 		} 	} 	http.Handle("/", changeHeaderThenServe(http.FileServer(http.Dir(".")))) 	http.ListenAndServe(":8000", nil) }   

Обработку ошибок убрал для простоты. На 8000-м порту в корне у Вас будет листинг директории, где запущено приложение с возможностью скачать любой файл.

Вызов контроллера по переданному имени

У меня получилось 170 контроллеров. Каждый контроллер вызывается через один из 6-и HandleFunc, например content.go. Имя контроллера в HandleFunc берется из POST или GET запроса вот так tplName := r.FormValue(«tpl_name»). Затем вызывается CallController(c, tplName), который вызывает один из 170 контроллеров, который в свою очередь выдает готовый html-код, а content.go получив HTML записывает его через w.Write([]byte(html)) и выдает браузеру. Ну как-то так.

Про Катю

Поздний вечер, я уверен, что Катя останется на ночь. Но она почему-то стала твердить, что ей надо домой. Я сказал, что хочу накормить её завтраком в постели. Договорись, что завтра утром она приедет завтракать. Вызвал такси, она уехала. Уснул счастливым, т.к. нашел себе красивую девушку, с которой весело и интересно. На следующий день от неё пришла смс-ка «Привет. Чем занимаешься?». Помню свои ощущения, ведь мне написала девушка, которая мне очень нравится, это было кайфово. И скоро мы с ней должны были снова встретиться…

Сессии

Благодаря модулю session от beego всё очень просто

// Сессии у меня хранятся в памяти, но можно хранить в файлах или БД globalSessions, _ = session.NewManager("memory", `{"cookieName":"gosessionid","gclifetime":86400}`) go globalSessions.GC() // Пишем в сессию   sess.Set("username", 1000) sess, _ := globalSessions.SessionStart(w, r) defer sess.SessionRelease(w) // Читаем из сессии username := sess.Get("username")

Обработка файлов

В контроллере upload_video.go мне понадобилось принять файл.

// Выделяем память под файл. 32Mb r.ParseMultipartForm(32 << 20) // Получаем файл в multipart.File file, _, _ := r.FormFile("file") buffer := new(bytes.Buffer) // Копируем в буфер _, err = io.Copy(buffer, file) defer file.Close() // Сам файл binaryFile = buffer.Bytes() // Название файла fileName := r.MultipartForm.File["file"][0].Filename // Content-Type contentType := r.MultipartForm.File["file"][0].Header.Get("Content-Type")   
Код сервера, который получает от клиента 3gp и возвращает mp4 (вдруг кому-то пригодится)

package main  import ( 	"io" 	"io/ioutil" 	"fmt" 	"net" 	"github.com/c-darwin/dcoin-go/packages/utils"         "os" 	"os/exec" )  func handleRequest(conn net.Conn) { 	// размер данных 	buf := make([]byte, 4) 	n, err := conn.Read(buf) 	if err != nil { 		fmt.Printf("%v", utils.ErrInfo(err)) 	} 	size := utils.BinToDec(buf) 	fmt.Printf("get data size: %v / n: %v\n", size, n) 	if size < 10485760 { 		// сами данные 		binaryData := make([]byte, size) 		n, err = io.ReadFull(conn, binaryData) 		fmt.Printf("n: %v\n", n) 		if err != nil { 			fmt.Printf("%v", utils.ErrInfo(err)) 		}  		gp3, err := ioutil.TempFile(os.TempDir(), "temp") 		if err != nil { 			fmt.Printf("%v", utils.ErrInfo(err)) 		} 		mp4, err := ioutil.TempFile(os.TempDir(), "temp") 		if err != nil { 			fmt.Printf("%v", utils.ErrInfo(err)) 		} 		err = ioutil.WriteFile(gp3.Name()+".3gp", binaryData, 0644) 		if err != nil { 			fmt.Printf("%v", utils.ErrInfo(err)) 		} 		out, err := exec.Command("/usr/bin/ffmpeg", "-i", gp3.Name()+".3gp", mp4.Name()+".mp4").Output() 		if err != nil { 			fmt.Println("/usr/bin/ffmpeg", "-i", gp3.Name()+".3gp", mp4.Name()+".mp4") 			fmt.Printf("%v\n", utils.ErrInfo(err)) 		} 		fmt.Printf("out: %v\n", out) 		 		data, err := ioutil.ReadFile(mp4.Name()+".mp4") 		if err != nil { 			fmt.Println(err) 		} 		// в 4-х байтах пишем размер данных, которые пошлем далее 		size := utils.DecToBin(len(data), 4) 		n, err = conn.Write(size) 		if err != nil { 			fmt.Println(err) 		} 	  	fmt.Printf("n: %v\n", n) 		 		// далее шлем сами данные 		n, err = conn.Write(data) 		if err != nil { 			fmt.Println(err) 		} 	  	fmt.Printf("n: %v\n", n)  	} }  func main() {     // включаем листинг TCP-сервером и обработку входящих запросов     l, err := net.Listen("tcp", ":8099")     if err != nil {    	fmt.Printf("Error listening: %v\n", err) 	panic(err) 	os.Exit(1)     }     defer l.Close()     for {         conn, err := l.Accept()         if err != nil {             fmt.Println("Error accepting: ", err.Error())             os.Exit(1)         }         go handleRequest(conn)     } } 

Преобразование Base64 в картинку

В crop_photo.go мне понадобилось принять картинку в base64 и получить из неё обычный png

b64, _ := base64.StdEncoding.DecodeString(r.FormValue("b64Img")) img, _, _ := image.Decode(bytes.NewReader(b64)) out, _ := os.Create("img.png") // в img.png сохраняется наша картинка png.Encode(out, img)

Про Катю

В тот день мы так и не встретились. И на следующий тоже. Я не знаю, что у неё происходило в голове, но она постоянно переносила свидания. Даже не поздравила с 23 февраля.
Решил её как-то растормошить. Написал Кате смс-ку: «скоро буду, конечно захвачу»
Она ответила: «в смысле?»
Я: «упс, не туда»
Она: «вот значит как»
У неё включилась ревность. Через день предложил встретиться, она согласилась.

Bindata

Когда скомпилил веб-сервер и перенес на одну из нод, то ничего не работало. Выяснилось, что все шаблоны, картинки и пр. сами в бинарник не запаковываются.
Пугуглил, нашел https://github.com/jteeuwen/go-bindata
Тулза очень удобная и простая, она генерит go-файл, где шаблоны, картинки и пр. пишутся в переменные в виде набора байт. В итоге после компиляции получается один бинарник. Если нужно чтобы файлы брались с диска, то надо добавить параметр "-debug=true".
Я использую простенький bash-скрипт, чтобы каждый раз не вводить путь, куда класть go-файл

#!/bin/bash if [ $# -gt 0 ] && [ $1 = "debug" ]  then   DEBUG="-debug=true" fi go-bindata -o="packages/static/static.go" -pkg="static" $DEBUG static/... 

Затем мне понадобилось, чтобы файлы из директории static можно было получать запросом localhost:8089/static/img.png. Для этого есть go-bindata-assetfs. Вот пример для директории /static/:

http.Handle("/static/", http.FileServer(&assetfs.AssetFS{Asset: static.Asset, AssetDir: static.AssetDir, Prefix: ""})) 

Про Катю

Проанализировав её поведение и, немного погуглив, я наткнулся вот на эту книгу "Новые правила. Секреты успешных отношений для современных девушек"
За вечер прочитал и понял, что она меня зачем-то в себя влюбляет используя советы из книжки.
Продолжение в следующей части.

Заключение

В следующих статьях я расскажу про html/template, БД, плавное завершение приложения через сигналы, обработку блоков из блокчейна, шифрование в GO и расшифровку в JS, про то, как я, немного изменил gomobile, добавив уведомления и работу в фоне для IOS и Android приложений.

ссылка на оригинал статьи http://habrahabr.ru/post/272695/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *