Стандарт макета пакета Golang

Go Программа перевода самородков
Стандарт макета пакета Golang

Обычно Vendoring используется как инструмент управления пакетами. Есть некоторые важные проблемы, которые уже можно увидеть в сообществе Go, но одна проблема, которая редко упоминается в сообществе, — это макет пакета приложения.

Каждое приложение Go, над которым я когда-либо работал, дает разные ответы на этот вопрос.Как я могу организовать свой код?. Некоторые приложения помещают все в один пакет, в то время как другие предпочитают организовывать код по типу или модулю. Без стратегии, применимой ко всей команде, вы обнаружите, что код разбросан по разным пакетам вашего приложения. Нам нужен лучший стандарт для дизайна макета пакета приложений Go.

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


Обновление: я получил много отличных отзывов на этом пути, где большинство из них хотят видеть приложение, построенное таким образом. Поэтому я начал переписать серию статей для использования этой планировки пакета для создания приложений, называемыхBuilding WTF Dial.

Распространенные ошибочные способы

Кажется, существует несколько распространенных способов организации приложений Go, и все они имеют свои подводные камни.

Метод № 1: Единая упаковка

Поместите весь свой код в один пакет, и он отлично подойдет для небольшого приложения. Это устраняет возможность проблем с циклическими зависимостями, поскольку в коде вашего приложения нет зависимостей.

Я видел, как приложения, построенные таким образом, содержали более 10 000 строк кода.SLOC. Но как только количество кода превышает это количество, становится очень трудно найти и изолировать ваш код.

Метод № 2: макет в стиле Rails

Другой способ организации кода — по функциональному типу. Например, поместите все своипроцессор, контроллер и код модели помещены в отдельные пакеты. Я видел много раньшеRailsРазработчики (включая меня) используют этот метод для организации своего кода.

Но есть две проблемы с использованием этого способа. Во-первых, ваше имя будет отстойным, и в итоге вы получите что-то вродеcontroller.UserControllerТакое именование, в котором вы повторяете имя пакета и имя типа. Когда дело доходит до именования, я одержимый человек. Я считаю, что имена — ваша лучшая документация, когда вы удаляете мертвый код. Хорошее имя также свидетельствует о качественном коде, который всегда первым замечают, когда другие люди читают код.

Большая проблема — циклические зависимости. Возможно, вашим различным функциональным типам потребуется ссылаться друг на друга. Это приложение будет работать только в том случае, если вы поддерживаете односторонние зависимости, но во многих случаях поддержание односторонних зависимостей не является тривиальной задачей.

Способ № 3: Организуйте свой код по модулям

Этот подход похож на предыдущий макет в стиле Rails, но вместо функциональности мы используем модули для организации кода. Например, у вас может бытьuserсумка иaccountСумка.

Мы обнаружили, что использование этого метода также столкнулось с той же проблемой, что и раньше. Мы также получаем что-то вродеusers.User.Такое ужасное имя. если нашaccounts.Controllerнужно иusers.Controllerвзаимодействуют, то мы также имеем ту же проблему циклической зависимости, и наоборот.

лучший способ

Стратегия организации пакетов, которую я использую в своем проекте, включает следующие 4 простых принципа:

  1. Корневой пакет для типов доменов
  2. Организация подпакетов по зависимостям
  3. использовать общийmockподпакет
  4. MainПакеты связывают зависимости вместе

Эти правила помогают изолировать наши пакеты и определить четкий доменный язык во всем приложении. Давайте посмотрим, как эти правила используются на практике.

№ 1. Пакет Root предназначен для типов доменов.

Ваше приложение использует язык высокого уровня, описывающий логический уровень взаимодействия данных и процессов. Это ваш домен. Если у вас есть приложение для электронной коммерции, ваши домены будут связаны с клиентами, учетными записями, платежами по кредитным картам и запасами. Если вашим приложением является Facebook, вашими доменами будут пользователи, лайки и отношения между пользователями. Это вещи, которые не зависят от вашей базовой технологии.

Я поместил свой тип домена в корень, чтобы сохранить. Этот пакет содержит только простые типы данных, такие как информация о пользователе.UserСтруктура или пользовательские данные, сохраненные изUserServiceинтерфейс.

Корневой пакет будет выглядеть так:

package myapp

type User struct {
	ID      int
	Name    string
	Address Address
}

type UserService interface {
	User(id int) (*User, error)
	Users() ([]*User, error)
	CreateUser(u *User) error
	DeleteUser(id int) error
}
скопировать код

Это делает ваш корневой пакет очень простым. Вы также можете поместить в этот пакет типы, содержащие операции, но они должны зависеть только от других типов полей. Например, в этот пакет можно добавить периодический опрос.UserServiceтип. Однако он не должен вызывать внешние службы или сохранять данные в базу данных. Это детали реализации.

Корневой пакет не должен зависеть ни от каких других пакетов в вашем приложении.

# 2. Организация подразделений по зависимостям

Если ваш корневой пакет не допускает внешних зависимостей, мы должны поместить эти зависимости в подпакеты. В этом макете пакета подпакеты действуют как адаптеры между вашим доменом и вашей реализацией.

Например, вашUserServiceВозможно, поддерживается базой данных PostgreSQL. Вы можете представитьpostgresподпакеты используются для предоставленияpostgres.UserServiceреализация.

package postgres

import (
	"database/sql"

	"github.com/benbjohnson/myapp"
	_ "github.com/lib/pq"
)

// UserService represents a PostgreSQL implementation of myapp.UserService.
type UserService struct {
	DB *sql.DB
}

// User returns a user for a given id.
func (s *UserService) User(id int) (*myapp.User, error) {
	var u myapp.User
	row := db.QueryRow(`SELECT id, name FROM users WHERE id = $1`, id)
	if row.Scan(&u.ID, &u.Name); err != nil {
		return nil, err
	}
	return &u, nil
}

// implement remaining myapp.UserService interface...
скопировать код

Это изолирует нашу зависимость от PostgreSQL, что упрощает тестирование и предоставляет нам простой способ миграции на другие базы данных в будущем. Если вы планируете поддерживать что-то вродеBoltDBРеализация такого рода базы данных может рассматриваться как загтузащитная архитектура.

Это также дает вам возможность реализовать иерархии. Допустим, вы хотите добавить кеш в памяти перед Postgresql.LRU cache. вы можете добавитьUserCacheвведите, чтобы обернуть вашу реализацию Postgresql.

package myapp

// UserCache wraps a UserService to provide an in-memory cache.
type UserCache struct {
        cache   map[int]*User
        service UserService
}

// NewUserCache returns a new read-through cache for service.
func NewUserCache(service UserService) *UserCache {
        return &UserCache{
                cache: make(map[int]*User),
                service: service,
        }
}

// User returns a user for a given id.
// Returns the cached instance if available.
func (c *UserCache) User(id int) (*User, error) {
	// Check the local cache first.
        if u := c.cache[id]]; u != nil {
                return u, nil
        }

	// Otherwise fetch from the underlying service.
        u, err := c.service.User(id)
        if err != nil {
        	return nil, err
        } else if u != nil {
        	c.cache[id] = u
        }
        return u, err
}
скопировать код

Мы также можем видеть код, организованный таким образом в стандартной библиотеке.io. ReaderПрочитанный байт — это тип поля, это достигается путем организации зависимостейtar.Reader,gzip. Reader,multipart.Readerбыть реализованным. Иерархические пути также можно увидеть в стандартной библиотеке, частоos.Fileодеялоbufio.Reader,gzip. Reader,tar.ReaderТакая иерархия пакетов.

зависимости между зависимостями

Зависимости не изолированы. ты можешь поставитьUserДанные хранятся в Postgresql, а данные о финансовых транзакциях — вStripeтакие сторонние сервисы. В этом случае мы инкапсулируем зависимость Stripe с типом логического домена, назовем егоTransactionService.

поставив нашTransactionServiceдобавить вUserServiceМы разделили наши две зависимости.

type UserService struct {
        DB *sql.DB
        TransactionService myapp.TransactionService
}
скопировать код

Теперь наши зависимости взаимодействуют только через язык общего домена. Это означает, что мы можем переключить Postgresql на MySQL или Strip на другой внутренний платежный процессор, не беспокоясь о том, что это повлияет на другие зависимости.

Не добавляйте это ограничение только к сторонним зависимостям.

Это может показаться странным, но я также использую это, чтобы изолировать зависимости от стандартной библиотеки. Напримерnet/httpПакет — это просто еще одна зависимость. Мы можем сделать это, включивhttpsubpackage, чтобы изолировать зависимости от него.

Может показаться странным иметь пакет с тем же именем, что и зависимости, которые он обертывает, но это всего лишь внутренняя реализация. если вы не разрешите другим частям вашего приложения использоватьnet/http, иначе в вашем приложении не будет конфликтов имен. копироватьhttpПрелесть названия в том, что оно требует, чтобы вы изолировали весь код, связанный с HTTP, вhttpв сумке.

package http

import (
        "net/http"
        
        "github.com/benbjohnson/myapp"
)

type Handler struct {
        UserService myapp.UserService
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        // handle request
}
скопировать код

теперь твойhttp.HandlerКак адаптер перед доменами и протоколом HTTP.

# 3. Совместное использование фиктивных подпакетов

Поскольку наши зависимости изолированы от других зависимостей через интерфейс домена, мы можем использовать эти точки подключения для внедрения аналоговой реализации.

Вот несколько лайковGoMockБиблиотека mocking, которая поможет вам генерировать фиктивные данные, но лично я предпочитаю писать свои собственные. Я считаю, что многие инструменты моделирования слишком сложны.

Макет, который я использую, очень прост. Например, параUserServiceСимуляция выглядит так:

package mock

import "github.com/benbjohnson/myapp"

// UserService represents a mock implementation of myapp.UserService.
type UserService struct {
        UserFn      func(id int) (*myapp.User, error)
        UserInvoked bool

        UsersFn     func() ([]*myapp.User, error)
        UsersInvoked bool

        // additional function implementations...
}

// User invokes the mock implementation and marks the function as invoked.
func (s *UserService) User(id int) (*myapp.User, error) {
        s.UserInvoked = true
        return s.UserFn(id)
}

// additional functions: Users(), CreateUser(), DeleteUser()
скопировать код

Это симуляция позволяет мне вводить функции для любого использованияmyapp.UserServiceинтерфейс для проверки параметров, возврата ожидаемых данных или неудачной инъекции.

Предположим, мы хотим протестировать то, что мы построили вышеhttp.Handler:

package http_test

import (
	"testing"
	"net/http"
	"net/http/httptest"

	"github.com/benbjohnson/myapp/mock"
)

func TestHandler(t *testing.T) {
	// Inject our mock into our handler.
	var us mock.UserService
	var h Handler
	h.UserService = &us

	// Mock our User() call.
	us.UserFn = func(id int) (*myapp.User, error) {
		if id != 100 {
			t.Fatalf("unexpected id: %d", id)
		}
		return &myapp.User{ID: 100, Name: "susy"}, nil
	}

	// Invoke the handler.
	w := httptest.NewRecorder()
	r, _ := http.NewRequest("GET", "/users/100", nil)
	h.ServeHTTP(w, r)
	
	// Validate mock.
	if !us.UserInvoked {
		t.Fatal("expected User() to be invoked")
	}
}
скопировать код

Наши макеты полностью изолируют наши модульные тесты, позволяя нам тестировать только обработку протокола HTTP.

#4. MainПакеты связывают зависимости вместе

Когда все эти зависимости поддерживаются независимо, вы можете задаться вопросом, как объединить их вместе. Этоmainпакетная работа.

Макет основного пакета

Приложение может создавать несколько двоичных файлов, поэтому мы используем соглашения Go, чтобы поместить нашиmainупаковать какcmdподкаталог пакета. Например, наш проект может иметьmyappслужебные двоичные файлы и один для управления службами на терминалеmyappctlКлиентские двоичные файлы. Наш пакет понравится этот макет:

myapp/
    cmd/
        myapp/
            main.go
        myappctl/
            main.go
скопировать код

Внедрять зависимости во время компиляции

Термин «внедрение зависимостей» стал плохим термином, он вызывает в воображенииSpringДлинные XML-файлы. Однако на самом деле этот термин означает просто передать зависимость нашему объекту, а не просить объект создать или найти саму зависимость.

существуетmainВ пакете мы можем выбрать, какие зависимости внедрять в какие объекты. так какmainПакет - это просто простое соединение, такmainКод часто относительно мал и тривиален.

package main

import (
	"log"
	"os"
	
	"github.com/benbjohnson/myapp"
	"github.com/benbjohnson/myapp/postgres"
	"github.com/benbjohnson/myapp/http"
)

func main() {
	// Connect to database.
	db, err := postgres.Open(os.Getenv("DB"))
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Create services.
	us := &postgres.UserService{DB: db}

	// Attach to HTTP handler.
	var h http.Handler
	h.UserService = us
	
	// start http server...
}
скопировать код

обратите внимание на вашmainВажно, чтобы в упаковке был еще и переходник. Он подключает все терминалы к вашему домену.

в заключении

Дизайн приложений — сложная задача. Несмотря на все принятые дизайнерские решения, без четкого набора принципов, которыми вы будете руководствоваться, ваши проблемы будут только усугубляться. Мы перечислили несколько способов проектирования макетов приложений Go и увидели множество их подводных камней.

Я считаю, что глядя на дизайн с точки зрения зависимостей, делает кодовую организацию проще и легче понять. Сначала мы проектируем наш доменный язык, то мы изолируем наши зависимости, то мы вводим, используя их, чтобы выделить наши тесты, и, наконец, мы поместим все вmainзавязанный в мешке.

Примите во внимание эти принципы при разработке следующего приложения. Если у вас есть какие-либо вопросы или вы хотите обсудить дизайн, пишите в Twitter @benbjohnsonсвяжитесь со мной, или вGopher slackнайтиbenbjohnsonНайди меня.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчан,продукт,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.