2017年にGoでWebアプリを作成します。 パート2


したがって、アプリケーションには、クライアントとサーバーの2つの主要部分があります。 (今は何年ですか?)。 サーバー側はGoになり、クライアント側はJSになります。 最初にサーバー側について話しましょう。


Go(サーバー)


アプリケーションのサーバー側は、JavaScriptに必要なすべてのもの、およびJSONファイル形式の静的ファイルやデータなど、その他すべての初期メンテナンスを担当します。 それだけです。(1)静的と(2)JSONの2つの機能だけです。


静的メンテナンスはオプションであることに注意してください。たとえば、静的はCDNで処理できます。 しかし、重要なことは、これはGoアプリケーションにとって問題ではないということです。Python/ Rubyアプリケーションとは異なり、NgnixやApacheが静的にサービスを提供するのと同等に機能します。 負荷を軽減するために静的ファイルの配布を他のアプリケーションに委任することは、特に必要ではありませんが、状況によっては意味があります。


単純化するために、データベーステーブルに保存された人々(姓と名のみ)のリストを提供するアプリケーションを作成していると想像してみましょう。 コードはこちら-https://github.com/grisha/gowebapp


ディレクトリ構造


私の経験が示すように、Goの初期段階でパッケージ間で機能を共有することは良い考えです。 最終バージョンがどのように構成されているかが明確でない場合でも、可能であればすべてを展開しておくことが最善です。


私の意見では、Webアプリケーションの場合、次のレイアウトが理にかなっています。


# github.com/user/foo foo/ # package main | +--daemon/ # package daemon | +--model/ # package model | +--ui/ # package ui | +--db/ # package db | +--assets/ #    JS    

トップレベル: mainパッケージ


最上位にはmainパッケージがあり、そのコードはmain.goファイルにあります。 主なgo get github.com/user/fooは、このような状況では、アプリケーション全体を$GOPATH/binにインストールするために必要なコマンドはgo get github.com/user/fooのみであるということです。


mainパッケージはできるだけ小さくする必要があります。 ここでの唯一のコードは、コマンド引数の分析です。 アプリケーションに設定ファイルがある場合、このファイルの解析と検証を別のパッケージに配置します。これはおそらくconfigを呼び出します。 その後、 mainは制御をdaemonパッケージに転送する必要がありdaemon


main.gomain.goです。


 package main import ( "github.com/user/foo/daemon" ) var assetsPath string func processFlags() *daemon.Config { cfg := &daemon.Config{} flag.StringVar(&cfg.ListenSpec, "listen", "localhost:3000", "HTTP listen spec") flag.StringVar(&cfg.Db.ConnectString, "db-connect", "host=/var/run/postgresql dbname=gowebapp sslmode=disable", "DB Connect String") flag.StringVar(&assetsPath, "assets-path", "assets", "Path to assets dir") flag.Parse() return cfg } func setupHttpAssets(cfg *daemon.Config) { log.Printf("Assets served from %q.", assetsPath) cfg.UI.Assets = http.Dir(assetsPath) } func main() { cfg := processFlags() setupHttpAssets(cfg) if err := daemon.Run(cfg); err != nil { log.Printf("Error in main(): %v", err) } } 

上記のコードは、 -listen-db-connect 、および-assets-path 3つのパラメーターを取ります。特別なものはありません。


明快さのための構造の使用


cfg := &daemon.Config{}行で、 daemon.Configオブジェクトを作成します。 その主な目標は、構造化された理解可能な形式で構成を提示することです。 各パッケージは、独自のタイプのConfig定義します。これは、必要なパラメーターを記述し、他のパッケージの設定を含めることができます。 上記のprocessFlags()にこの例があります: flag.StringVar(&cfg.Db.ConnectString, ...ここでdb.Config含まれていdaemon.Config 。私の意見では、これは非常に便利なトリックです。 JSON、TOML、またはその他の形式で。


http.FileSystemを使用して静的データを提供する


http.Dir(assetsPath)は、 uiパッケージで静的を提供する方法の準備です。 これは、 cfg.UI.Assetshttp.FileSystemインターフェースである)の別の実装のためのスペースを残すような方法で行われます。たとえば、RAMからこのコンテンツを提供します。 これについては、後ほど別の投稿で詳しく説明します。


最後に、 maindaemon.Run(cfg)呼び出します。これは実際にアプリケーションを起動し、 daemon.Run(cfg)までロックdaemon.Run(cfg)ます。


daemonパッケージ


daemonパッケージには、プロセスの開始に関連するすべてが含まれています。 これには、たとえば、どのポートがリッスンされるか、ユーザーログがここで定義されるほか、ポライトリスタートなどに関連するすべてが含まれます。


daemonパッケージのタスクはデータベースへの接続を初期化することなので、 dbパッケージをインポートする必要があります。 彼は、TCPポートをリッスンし、このリスナーのユーザーインターフェイスを起動することも担当しているため、 uiパッケージをインポートする必要がありますuiパッケージは、 modelパッケージによって提供されるデータにアクセスする必要があるため、 modelパッケージもインポートする必要があります。


daemonモジュールのスケルトンは次のようになります。


 package daemon import ( "log" "net" "os" "os/signal" "syscall" "github.com/grisha/gowebapp/db" "github.com/grisha/gowebapp/model" "github.com/grisha/gowebapp/ui" ) type Config struct { ListenSpec string Db db.Config UI ui.Config } func Run(cfg *Config) error { log.Printf("Starting, HTTP on: %s\n", cfg.ListenSpec) db, err := db.InitDb(cfg.Db) if err != nil { log.Printf("Error initializing database: %v\n", err) return err } m := model.New(db) l, err := net.Listen("tcp", cfg.ListenSpec) if err != nil { log.Printf("Error creating listener: %v\n", err) return err } ui.Start(cfg.UI, m, l) waitForSignal() return nil } func waitForSignal() { ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) s := <-ch log.Printf("Got signal: %v, exiting.", s) } 

前述のとおり、 Configdb.Configui.Config含まれていることに注意してください。


すべてのアクションはRun(*Config)行われRun(*Config) 。 データベースへの接続を初期化し、 model.Modelインスタンスを作成してuiを実行し、設定、モデルおよびリスナーへのポインターを渡します。


modelパッケージ


modelの目的は、データベースへのデータの格納方法をuiから分離し、アプリケーションが持つことができるビジネスロジックを提供することです。 これがアプリケーションの頭脳です。


modelパッケージは構造を定義する必要があり( Modelは適切な名前のように見えます)、この構造のインスタンスへのポインターをすべてのui関数とメソッドに渡す必要があります。 アプリケーションにはこのようなインスタンスが1つだけ存在する必要があります。さらに自信を持たせるために、シングルトンの助けを借りてプログラムで実装できますが、それほど必要ではないと思います。


または、 Model構造を使用せずに、 modelパッケージ自体を使用することもできます。 私はこのアプローチが好きではありませんが、これはオプションです。


モデルは、処理するデータエンティティの構造も定義する必要があります。 この例では、これはPerson構造になります。 そのメンバーは、他のパッケージがアクセスするため、エクスポート(大文字化)する必要があります。 sqlxを使用する場合、ここでは、 db:"first_name"など、データベース内の列名に構造要素をバインドするタグを指定する必要があります。


私たちのタイプはPersonです:


 type Person struct { Id int64 First, Last string } 

列名は構造要素の名前に対応し、 sqlxがレジスタを処理するため、ここではタグは必要ありません。これにより、 Last lastという名前の列に対応します。


modelパッケージはdbインポートしないでください


やや直感に反して、 modeldbインポートすべきではありません。 しかし、 dbパッケージはmodelをインポートする必要があり、Goではインポートのループが禁止されているため、そうすべきではありません。 これは、インターフェイスが便利な場合です。 model dbが満たす必要のあるインターフェイスを指定する必要があります。 これまでのところ、人のリストが必要であることがわかっているだけなので、この定義から始めることができます。


 type db interface { SelectPeople() ([]*Person, error) } 

私たちのアプリケーションはそれほど多くはしませんが、人がリストされていることを知っているので、私たちのモデルはおそらくPeople() ([]*Person, error)メソッドを持つべきです:


 func (m *Model) People() ([]*Person, error) { return m.SelectPeople() } 

すべてをきれいに保つには、コードを別のファイルに配置することをおperson.goます。たとえば、 Person構造はperson.goなどで定義する必要があります。 しかし、読みやすくするために、 modelパッケージの単一ファイルバージョンを以下に示します。


 package model type db interface { SelectPeople() ([]*Person, error) } type Model struct { db } func New(db db) *Model { return &Model{ db: db, } } func (m *Model) People() ([]*Person, error) { return m.SelectPeople() } type Person struct { Id int64 First, Last string } 

dbパッケージ


dbは、データベースと対話する実際の実装です。 これは、SQLステートメントが構築および実行される場所です。 このパッケージは、 model 、p.chもインポートします。 データベースデータからこれらの構造を作成する必要があります。


まず、 dbInitDB関数を提供する必要がありますInitDB関数は、データベースへの接続を確立し、必要なテーブルを作成してSQLクエリを準備します。


単純化された例では移行をサポートしていませんが、理論的には、これは移行を実行する必要がある場所です。


PostgreSQLを使用しているため、 pqドライバーをインポートする必要があります。 また、 sqlx依存し、 modelが必要になります。 db実装の開始点は次のとおりです。


 package db import ( "database/sql" "github.com/grisha/gowebapp/model" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) type Config struct { ConnectString string } func InitDb(cfg Config) (*pgDb, error) { if dbConn, err := sqlx.Connect("postgres", cfg.ConnectString); err != nil { return nil, err } else { p := &pgDb{dbConn: dbConn} if err := p.dbConn.Ping(); err != nil { return nil, err } if err := p.createTablesIfNotExist(); err != nil { return nil, err } if err := p.prepareSqlStatements(); err != nil { return nil, err } return p, nil } } 

エクスポートされた関数InitDb()は、 model.dbインターフェースのPostgres実装であるpgDbインスタンスを作成します。 準備されたクエリを含む、データベースとの通信に必要なすべてが含まれ、インターフェイスに必要なメソッドを実装します。


 type pgDb struct { dbConn *sqlx.DB sqlSelectPeople *sqlx.Stmt } 

以下は、テーブルを作成し、クエリを準備するためのコードです。 SQLの観点から見ると、すべてが非常に単純化されており、もちろん改善すべき点がたくさんあります。


 func (p *pgDb) createTablesIfNotExist() error { create_sql := ` CREATE TABLE IF NOT EXISTS people ( id SERIAL NOT NULL PRIMARY KEY, first TEXT NOT NULL, last TEXT NOT NULL); ` if rows, err := p.dbConn.Query(create_sql); err != nil { return err } else { rows.Close() } return nil } func (p *pgDb) prepareSqlStatements() (err error) { if p.sqlSelectPeople, err = p.dbConn.Preparex( "SELECT id, first, last FROM people", ); err != nil { return err } return nil } 

最後に、インターフェイスを実装するメソッドを提供する必要があります。


 func (p *pgDb) SelectPeople() ([]*model.Person, error) { people := make([]*model.Person, 0) if err := p.sqlSelectPeople.Select(&people); err != nil { return nil, err } return people, nil } 

ここでは、 sqlxを利用してクエリを実行し、 Select()呼び出すだけで結果からスライスを作成します(注: p.sqlSelectPeople*sqlx.Stmt*sqlx.Stmt )。 sqlxを使用しないsqlx結果行を反復処理し、 Scanを使用して各行を処理する必要があり、これはより冗長になります。


非常に微妙な点に注意してください。 peoplevar people []*model.Personとして定義でき、メソッドは同じように機能します。 ただし、データベースが空の行セットを返す場合、メソッドは空のスライスではなくnilを返します。 このメソッドの結果が後でJSONでエンコードされると、 []ではなくnullになりnull 。 これは、クライアント側がnull処理方法を知らない場合に問題を引き起こす可能性がありnull


dbです。


uiパッケージ


最終的には、HTTPを介してこれらすべてを提供する必要があり、それがまさにuiパッケージの機能です。


これは非常に簡略化されたバージョンです。


 package ui import ( "fmt" "net" "net/http" "time" "github.com/grisha/gowebapp/model" ) type Config struct { Assets http.FileSystem } func Start(cfg Config, m *model.Model, listener net.Listener) { server := &http.Server{ ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 16} http.Handle("/", indexHandler(m)) go server.Serve(listener) } const indexHTML = ` <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>Simple Go Web App</title> </head> <body> <div id='root'></div> </body> </html> ` func indexHandler(m *model.Model) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, indexHTML) }) } 

indexHTMLほとんど何もindexHTMLいないことに注意してください。 これは、アプリケーションが使用するHTMLのほぼ100%です。 アプリケーションのクライアント部分を起動すると、わずか数行で少し変化します。


ハンドラーの定義方法にも注意する必要があります。 このイディオムに慣れていない場合は、Goでは非常に一般的であるため、数分(または1日)かけて完全に理解する価値があります。 indexHandler()はハンドラーそのものではなく、ハンドラー関数を返します 。 これは、HTTPハンドラー関数の署名が固定されており、モデルへのポインターがパラメーターの1つではないため、クロージャーを介して*model.Modelを渡すことができるように行われます。


indexHandler()でモデルへのインデックスを使用して何もしていませんが、人々のリストの実際の実装に到達するとき、それが必要になります。


おわりに


実際、上記のリストは、少なくともGo側からGoで基本的なWebアプリケーションを作成するために知っておく必要があるすべてのものです。 次の記事では、クライアントの部分を取り上げ、人々のリストのコードを完成させます。


継続



Source: https://habr.com/ru/post/J329584/


All Articles