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.go



main.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.Assets



http.FileSystem



インターフェースである)の別の実装のためのスペースを残すような方法で行われます。たとえば、RAMからこのコンテンツを提供します。 これについては、後ほど別の投稿で詳しく説明します。







最後に、 main



daemon.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) }
      
      





前述のとおり、 Config



db.Config



ui.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



インポートしないでください



やや直感に反して、 model



db



インポートすべきではありません。 しかし、 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もインポートします。 データベースデータからこれらの構造を作成する必要があります。







まず、 db



InitDB



関数を提供する必要があります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



を使用して各行を処理する必要があり、これはより冗長になります。







非常に微妙な点に注意してください。 people



var 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アプリケーションを作成するために知っておく必要があるすべてのものです。 次の記事では、クライアントの部分を取り上げ、人々のリストのコードを完成させます。







継続








All Articles