发送 Index 请求
让我们添加创建图书的 API,然后放入一些测试数据供后续使用。
创建 domain/gateway/book_manager.go:
/*
Package gateway contains all domain gateways.
*/
package gateway
import (
"context"
"literank.com/fulltext-books/domain/model"
)
// BookManager manages all books
type BookManager interface {
IndexBook(ctx context.Context, b *model.Book) (string, error)
}
此处也是在使用4层架构。项目越大,该架构的好处越明显。阅读更多。
创建 infrastructure/search/es.go:
/*
Package search does all search engine related implementations.
*/
package search
import (
"context"
"github.com/elastic/go-elasticsearch/v8"
"literank.com/fulltext-books/domain/model"
)
const INDEX_BOOK = "book_idx"
// ElasticSearchEngine runs all index/search operations
type ElasticSearchEngine struct {
client *elasticsearch.TypedClient
pageSize int
}
// NewEngine constructs a new ElasticSearchEngine
func NewEngine(address string, pageSize int) (*ElasticSearchEngine, error) {
cfg := elasticsearch.Config{
Addresses: []string{address},
}
client, err := elasticsearch.NewTypedClient(cfg)
if err != nil {
return nil, err
}
// Create the index
return &ElasticSearchEngine{client, pageSize}, nil
}
// IndexBook indexes a new book
func (s *ElasticSearchEngine) IndexBook(ctx context.Context, b *model.Book) (string, error) {
resp, err := s.client.Index(INDEX_BOOK).
Request(b).
Do(ctx)
if err != nil {
return "", err
}
return resp.Id_, nil
}
我们此处使用 fully-typed API 客户端(elasticsearch.TypedClient
)来索引文档。
默认情况下,Elasticsearch 允许你将文档索引到尚不存在的索引中。 当你将文档索引到不存在的索引时,Elasticsearch 将会使用默认设置动态地创建索引。这在开发者不想显式地创建索引时会很方便。
安装 yaml
依赖:
go get -u gopkg.in/yaml.v3
创建 infrastructure/config/config.go:
/*
Package config provides config structures and parse funcs.
*/
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// Config is the global configuration.
type Config struct {
App ApplicationConfig `json:"app" yaml:"app"`
Search SearchConfig `json:"search" yaml:"search"`
}
// SearchConfig is the configuration of search engines.
type SearchConfig struct {
Address string `json:"address" yaml:"address"`
}
// ApplicationConfig is the configuration of main app.
type ApplicationConfig struct {
Port int `json:"port" yaml:"port"`
PageSize int `json:"page_size" yaml:"page_size"`
}
// Parse parses config file and returns a Config.
func Parse(filename string) (*Config, error) {
buf, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
c := &Config{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return nil, fmt.Errorf("failed to parse file %s: %v", filename, err)
}
return c, nil
}
创建 config.yml:
app:
port: 8080
page_size: 10
search:
address: "http://localhost:9200"
警醒:
不要直接 git 提交 config.yml。可能会导致敏感数据泄露。如果非要提交的话,建议只提交配置格式模板。
比如:app: port: 8080 search: address: ""
创建 application/executor/book_operator.go:
/*
Package executor handles request-response style business logic.
*/
package executor
import (
"context"
"literank.com/fulltext-books/domain/gateway"
"literank.com/fulltext-books/domain/model"
)
// BookOperator handles book input/output and proxies operations to the book manager.
type BookOperator struct {
bookManager gateway.BookManager
}
// NewBookOperator constructs a new BookOperator
func NewBookOperator(b gateway.BookManager) *BookOperator {
return &BookOperator{bookManager: b}
}
// CreateBook creates a new book
func (o *BookOperator) CreateBook(ctx context.Context, b *model.Book) (string, error) {
return o.bookManager.IndexBook(ctx, b)
}
创建 application/wire_helper.go:
/*
Package application provides all common structures and functions of the application layer.
*/
package application
import (
"literank.com/fulltext-books/domain/gateway"
"literank.com/fulltext-books/infrastructure/config"
"literank.com/fulltext-books/infrastructure/search"
)
// WireHelper is the helper for dependency injection
type WireHelper struct {
engine *search.ElasticSearchEngine
}
// NewWireHelper constructs a new WireHelper
func NewWireHelper(c *config.Config) (*WireHelper, error) {
engine, err := search.NewEngine(c.Search.Address, c.App.PageSize)
if err != nil {
return nil, err
}
return &WireHelper{engine}, nil
}
// BookManager returns an instance of BookManager
func (w *WireHelper) BookManager() gateway.BookManager {
return w.engine
}
创建 adapter/router.go:
/*
Package adapter adapts to all kinds of framework or protocols.
*/
package adapter
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"literank.com/fulltext-books/application"
"literank.com/fulltext-books/application/executor"
"literank.com/fulltext-books/domain/model"
)
// RestHandler handles all restful requests
type RestHandler struct {
bookOperator *executor.BookOperator
}
func newRestHandler(wireHelper *application.WireHelper) *RestHandler {
return &RestHandler{
bookOperator: executor.NewBookOperator(wireHelper.BookManager()),
}
}
// MakeRouter makes the main router
func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
rest := newRestHandler(wireHelper)
// Create a new Gin router
r := gin.Default()
r.POST("/books", rest.createBook)
return r, nil
}
// Create a new book
func (r *RestHandler) createBook(c *gin.Context) {
var reqBody model.Book
if err := c.ShouldBindJSON(&reqBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
bookID, err := r.bookOperator.CreateBook(c, &reqBody)
if err != nil {
fmt.Printf("Failed to create: %v\n", err)
c.JSON(http.StatusNotFound, gin.H{"error": "failed to create"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": bookID})
}
用以下代码替换 main.go 中内容:
package main
import (
"fmt"
"literank.com/fulltext-books/adapter"
"literank.com/fulltext-books/application"
"literank.com/fulltext-books/infrastructure/config"
)
const configFileName = "config.yml"
func main() {
// Read the config
c, err := config.Parse(configFileName)
if err != nil {
panic(err)
}
// Prepare dependencies
wireHelper, err := application.NewWireHelper(c)
if err != nil {
panic(err)
}
// Build main router
r, err := adapter.MakeRouter(wireHelper)
if err != nil {
panic(err)
}
// Run the server on the specified port
if err := r.Run(fmt.Sprintf(":%d", c.App.Port)); err != nil {
panic(err)
}
}
执行 go mod tidy
整理 go.mod
文件:
go mod tidy
再次运行 server,然后使用 curl
进行测试:
go run main.go
样例请求:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Da Vinci Code","author":"Dan Brown","published_at":"2003-03-18","content":"In the Louvre, a curator is found dead. Next to his body, an enigmatic message. It is the beginning of a race to discover the truth about the Holy Grail."}' \
http://localhost:8080/books
样例响应:
{"id":"C1Ok9I4BexLIwExhUXKX"}
放入测试数据
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Harry Potter and the Philosopher\u0027s Stone","author":"J.K. Rowling","published_at":"1997-06-26","content":"A young boy discovers he is a wizard and begins his education at Hogwarts School of Witchcraft and Wizardry, where he uncovers the mystery of the Philosopher‘s Stone."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"To Kill a Mockingbird","author":"Harper Lee","published_at":"1960-07-11","content":"Set in the American South during the Great Depression, the novel explores themes of racial injustice and moral growth through the eyes of young Scout Finch."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien","published_at":"1954-07-29","content":"A hobbit named Frodo Baggins embarks on a perilous journey to destroy a powerful ring and save Middle-earth from the Dark Lord Sauron."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Catcher in the Rye","author":"J.D. Salinger","published_at":"1951-07-16","content":"Holden Caulfield narrates his experiences in New York City after being expelled from prep school, grappling with themes of alienation, identity, and innocence."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Alchemist","author":"Paulo Coelho","published_at":"1988-01-01","content":"Santiago, a shepherd boy, travels from Spain to Egypt in search of a treasure buried near the Pyramids. Along the way, he learns about the importance of following one‘s dreams."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Hunger Games","author":"Suzanne Collins","published_at":"2008-09-14","content":"In a dystopian future, teenagers are forced to participate in a televised death match called the Hunger Games. Katniss Everdeen volunteers to take her sister‘s place and becomes a symbol of rebellion."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"1984","author":"George Orwell","published_at":"1949-06-08","content":"Winston Smith lives in a totalitarian society ruled by the Party led by Big Brother. He rebels against the oppressive regime but ultimately succumbs to its control."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"The Girl with the Dragon Tattoo","author":"Stieg Larsson","published_at":"2005-08-01","content":"Journalist Mikael Blomkvist and hacker Lisbeth Salander investigate the disappearance of a young woman from a wealthy family, uncovering dark secrets and corruption."}' \
http://localhost:8080/books
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"Gone Girl","author":"Gillian Flynn","published_at":"2012-06-05","content":"On their fifth wedding anniversary, Nick Dunne‘s wife, Amy, disappears. As the media circus ensues and suspicions mount, Nick finds himself in a whirlwind of deception and betrayal."}' \
http://localhost:8080/books