Итоги разработки сервера Go

задняя часть Go
Итоги разработки сервера Go

Разработка на стороне сервера обычно относится к подготовке бизнес-интерфейсов.Для большинства систем операции CURD в интерфейсах составляют большую часть из них. Однако в Интернете всегда ходят шутки про «инженеров CURD», чтобы показать, что такого рода технология разработки не сложна. Но я лично считаю, что это действительно просто, если вы просто найдете фреймворк для заполнения кода для выполнения задачи, но люди - это «мыслящий тростник».Если вы глубоко задумаетесь, вы все равно столкнетесь со многими вещами в процессе разработки. .общий вопрос. В качестве примера возьмем среду разработки go, которая имеет две формы дифференциации: типаbeegoКак представитель,goframeПродолжайте продвигать огромное количество типов фреймворков, они характеризуются большими и всеобъемлющими, предоставляющими множество функций, вам даже не нужно делать много выбора, в любом случае, просто используйте его в соответствии с документацией. В этом и проблема с ними.Много раз из-за хорошей упаковки многие проблемы решались незаметно (но не обязательно самое подходящее решение). Другойgin, go-mirco и другие фреймворки представлены, они решают только конкретную часть задачи, хотя с их помощью предстоит еще много дополнительной работы, но вы также можете узнать больше вещей. Далее давайте подробно рассмотрим проблемы, с которыми можно столкнуться при разработке go на стороне сервера:

1. Структура проекта

Будь то большой проект или небольшая система управления, первым шагом в «Долгом походе» является организация собственной структуры проекта. С точки зрения структуры проекта go фактически не имеет фиксированного критерия, поэтому его можно гибко организовать в соответствии с реальной ситуацией. Но я думаю, что есть еще некоторые моменты, о которых следует знать:

1. Название пакета простое, но обратите внимание на значение имени

этоэта статьяКак уже упоминалось в , используйте уточненные сокращения вместо длинных имен пакетов, которые также часто появляются в go.fmt,strconvи другие часто используемые пакеты сокращений, а такжеpkg,cmdЖдать. Но я думаю, что важнее знать смысл, чем быть простым. Например, однажды я взял на себя управление проектом, и его корневой каталог имеетmdwpackage, я сначала не знал, что это такое, но я увидел внутри него промежуточное ПО gin и понял, что этоmiddlewareаббревиатура от. Поэтому, хотя go официальной рекомендует использовать какие-то условные и лаконичные названия пакетов, следует добавить с посылкой, то есть в комментариях должна быть объяснена функция этого пакета, а комментариев в отечественной среде очень не хватает. Так что вместо того, чтобы создавать какие-то аббревиатуры и не писать комментарии, лучше четко написать название пакета.

2. Используйте внутренний

Использование внутреннего помогает заставить людей думать о том, что должно быть в публичных пакетах и ​​что должно быть в приватных пакетах, что делает структуру проекта более ясной. Более того, права доступа к пакетам, предоставляемые самим go, не такие детализированные, как в java, только public и private, их следует дополнить внутренними.

3. Не используйте init случайно

Честно говоря, у меня все еще есть некоторые сомнения относительно того, почему нет ограничений на инициализацию, то есть некоторая библиотека, от которой вы зависите, может запускаться до кода вашей программы, и вы понятия не имеете, что она будет делать (любой код может выполняться в init). Это проявляется в больших проектах с множеством зависимостей и косвенных зависимостей. Хотя Go официально требует не выполнять какую-либо сложную логику в инициализации, это не является обязательным. Самый простой пример это модульное тестирование.Иногда когда я запускаю модульное тестирование я часто сталкиваюсь с паникой что не может запуститься.Причина в том,что некоторые зависимые библиотеки init проделали какие-то хитрые операции. Но проблема в том, что я зависим (косвенно зависим) от этой библиотеки и не контролирую ее код (нет разрешения на изменение). В этом случае он может продолжать работать только в том случае, если его требования выполнены в модульном тесте. Так что поместите код в init, вы должны подумать дважды. Насколько я вижу, многие коды, использующие init, выполняют инициализацию, но они неявно зависят от файлов, путей, ресурсов и т. д. Подумайте об этой ситуации, можете ли вы вместо этого использовать такие функции, как NewXX() \ InitXX().

4. Будьте осторожны с именами пакетов, такими как util\common

Это обычно используется java-программистами, но на самом деле в go вместо этого бессмысленного имени пакета рекомендуется осмысленное имя пакета. Например:util.NewTimeHelper()Это не хорошо, это должно быть написано какtime_helper.New()Это делает его более читабельным. Но я думаю, что конкретную ситуацию нужно разобрать подробно, поэтому название используется не без осторожности. Потому что иногда ваша util\common — это всего лишь несколько вспомогательных функций, не так уж и много. Разделение на несколько пакетов ощущается немного больше, чем потеря, и еще не поздно провести рефакторинг после того, как util\common будет сохранен еще немного. Так что вернитесь к тому, что я упомянул в начале, подумайте больше и действуйте гибко. Но здесь мы должны снова обратить внимание: если это тип общедоступной утилиты, на которую полагаются многие люди, лучше разделить ее раньше, иначе ее нельзя будет разделить позже.

2. Структура кода

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

1. Деление c\s\d слоев

Благодаря популярности MVC, несмотря на популяризацию архитектуры разделения клиентской и серверной частей, большинство проектов все еще существуют внутри компании.controllerorhandler,serviceorsvc,daoorrepositoryтакое деление. Логика для изолированного представления данных, логической обработки и доступа к данным. Вот мое понимание трехуровневого деления:controller: Как правило, это контроллер входа, который выполняет прием параметров, преобразование, обработку возвращаемых значений, обработку протоколов и т. д. Этот слой вообще не слишком толстый, то есть логики будет не слишком много. Здесь следует обратить внимание на разницу между ним и шлюзом (Gateway), шлюз должен делать и может намного больше, чем он. Затем некоторые проекты будут помещать проверку параметров на этот уровень.Я лично считаю, что проверка параметров должна использовать некоторые фреймворки, такие какvalidatorДелайте это, не повторяйте колесо, если вам нужно получить доступ к базе данных для проверки параметров, его следует разместить вserviceслой сделать.service: Этот слой может быть тяжелее, и это также место для проверки навыков дизайна.Если вы не будете осторожны, этот слой легко сделать сильно связанным. я тоже виделserviceОперация прямого написания SQL-запросов в слое очень хлопотна. В общем, поскольку этот слой является связующим звеном, постарайтесь сделать его связующим, а не универсальным.dao: Этот уровень связан с данными, по сути, он заключается в том, чтобы превратить непосредственную работу с данными (операционная база данных, Redis) сервисным уровнем в вызов метода. Чтобы скрыть различия базы данных, она также может выполнять некоторую унифицированную обработку данных. Вообще говоря, наши проекты будут использовать orm, и этот слой также может один раз инкапсулировать orm, что упрощает его использование. Так как этот слой является более обобщенной обработкой данных, его обычно удобнее генерировать через генератор кода, например:gormt.

2. Транзитивность зависимостей

Зависимость здесь относится кcontroller,service,daoЗависимости третьего уровня, как правило,controllerнужно позвонитьservice,serviceнужно позвонитьdao. Самое запретное заключается в том, что, поскольку верхнему уровню нужен нижний уровень, код, создающий нижний уровень, вызывается на верхнем уровне, например, вcontrollerВызывается в конструкторе (то есть NewXX, специального конструктора в Go нет)NewService, что явно не соответствует принципу единой ответственности. Таким образом, обычно есть два способа справиться с этим: Во-первых, создание глобальных переменных

var XX *XXService = &XXService{}

type XXService struct{
}

func (x *XXService) XX() {
}

Таким образом, другие слои могут напрямую вызывать эту глобальную переменную. Удобство удобством, но оно также приносит две проблемы: 1. Звоните по желанию Таким образом, можно вызвать не только верхний уровень, но и нижний уровень. На самом деле его можно настраивать везде, что легко может привести к неуправляемому управлению, особенно в проектах, где много людей работает вместе. 2. Невозможно хранить поля в XXService Удержание поля должно включать в себя его инициализацию.Если оно помещено в init, как упоминалось выше, запускать init нехорошо. Если вы установитеNewXX()функцию, то вам не нужно устанавливать эту глобальную переменную. 2. Установите функцию NewXX() и управляйте ею через инфраструктуру внедрения зависимостей.

type XXService struct{
	xRepo XXRepo
}

func NewXXService(r *XXRepo) *XXService {
	
}

Затем эти конструкторы управляются через структуру внедрения зависимостей.

// wire.Build(repo.NewGoodsRepo, svc.NewGoodsSvc, controller.NewGoodsController)
// wire 框架自动生成
func initControllers() (*Controllers, error) {
	goodsRepo := repo.NewGoodsRepo()
	goodsSvc := svc.NewGoodsSvc( goodsRepo)
	goodsController := controller.NewGoodsController(goodsSvc)
	return goodsController, nil
}

Здесь инфраструктура проводов гораздо менее мощна, чем инфраструктура внедрения зависимостей в java, Фактически, она избавляет нас от необходимости писать ее самостоятельно. Таким образом, Контроллер содержит объект Службы, а Служба содержит объект Репо. Более того, его могут удерживать только те, кто зарегистрировался, что позволяет избежать путаницы в управлении.

3. Старайтесь избегать глобальных переменных

Когда дело доходит до проблемы глобальных переменных, мы должны выделить ее отдельно и тщательно обсудить. Наиболее типичным примером глобальных переменных является регистратор.Как мы все знаем, пакет журнала, предоставляемый go, не очень полезен, поэтому обычно мы будем использовать некоторые реализации регистратора с открытым исходным кодом, многие реализации предоставляют журнал по умолчанию (defaultLogger), который удобно пользоваться.не было ничего. Но поскольку обработка ошибок в Go сама по себе поддерживает обработку на месте, т. е.if err != nil, это фатально. В некоторых крупных проектах авторы многих функций и библиотек могут, оценив ошибку, просто распечатать журнал или везде распечатать журнал отладки нерегулярно. В результате, когда мы смотрим лог программы, мы можем увидеть много мусорных логов, типа: ошибка печатается несколько раз или печатается просто какая-то бесполезная ерунда. Количество логов в день может доходить до десятка G. Не знаю, насколько загружен бизнес, на самом деле это все бредовые логи. Лучше сделать этоzap, у него нет такого глобального логгера, приходится New объект, что заставляет думать, как сохранить этот объект? Как передать его в то место, где нужно распечатать лог? Но, к сожалению, я также видел объект журнала zap, напрямую назначенный глобальной переменной, а затем продолжающий использовать с ума. При использовании горма тоже встречается эта проблема, и возвращаемыйgorm.DBОбъекты выбрасываются в глобальные переменные и используются везде, что является той же проблемой, что и логгеры.

3. Обработка наблюдаемости

Наблюдаемость — это органичное сочетание журналов, отслеживания ссылок и мониторинга. Дополнительные сведения см. в некоторых ссылках в приложении. Наблюдаемость на самом деле известна публике после того, как егеря и прометеи стали популярными, хотя история не слишком длинная, ее важность очевидна. Он эффективно решает проблему быстрого поиска и определения местоположения конкретного места, когда возникает проблема с онлайн-сервисами, будь то большой проект, небольшой проект, микросервисная архитектура или отдельная архитектура, это очень необходимая часть. С точки зрения конкретного сотрудничества, во-первых, вы можете использовать службу мониторинга, такую ​​​​как prometheus, чтобы вовремя обнаружить аномалию службы, а затем сотрудничать с jaeger + log, чтобы найти контекст, когда возникает проблема, чтобы быстро найти ее. . В прошлом, когда я не использовал технологии отслеживания и мониторинга ссылок, я полагался только на логирование, и многие онлайн-ошибки не могли быть вовремя обнаружены или воспроизведены, что на самом деле очень жаль. Однако для достижения наблюдаемости какой-то код необходимо более или менее модифицировать, а полностью неинвазивная модификация невозможна, поэтому этот аспект можно учитывать на этапе проектирования проекта. Навязчиво, следите за журналом service->dao->db, может отслеживать всю ссылку запроса, лог, безусловно, самый навязчивый, и он печатается в бизнес-коде.

1. Обработка трассировки ссылок db\redis\log

Ядром отслеживания ссылок является контекст. Контекст отслеживания генерируется в начале запроса, и каждый уровень обрабатывает и передает контекст. Если это код внутри проекта, диапазон может быть проанализирован из контекста, а затем данные печатаются в диапазоне. Но для некоторых библиотек, от которых зависит проект (gorm\zap\redis и т.д.), если вы хотите проследить ссылку внутрь этих библиотек, есть два способа с этим справиться:

  1. Сама библиотека поддерживает передачу контекста Например, gorm может передать контекст в него.Хотя он не может помочь вам разобрать контекст, он предоставляет возможность ловушки.Вы можете написать плагин самостоятельно, чтобы получить контекст и обработать его самостоятельно. Или такой фреймворк, как go-micro, который автоматически занимается разбором контекста.
// gorm示例
	// 使用插件
	err = db.Use(NewPlugin(WithDBName(dbName)))
	if err != nil {
		return nil, err
	}
	
	// 查询
	DB.WithContext(ctx).Find()
  1. Библиотека не поддерживает передачу контекста Или библиотека поддерживает передачу контекста, но не предоставляет возможности подключения. Поскольку мы не можем модифицировать код библиотеки и не можем перехватывать ее внутренние ключевые операции, нам нужно взять на себя доступ к библиотеке через режим прокси, например: go-redis
type Repo interface {
	Set(ctx context.Context, key, value string, ttl time.Duration, options ...Option) error
	Get(ctx context.Context, key string, options ...Option) (string, error)
	TTL(ctx context.Context, key string) (time.Duration, error)
	Expire(ctx context.Context, key string, ttl time.Duration) bool
	ExpireAt(ctx context.Context, key string, ttl time.Time) bool
	Del(ctx context.Context, key string, options ...Option) bool
	Exists(ctx context.Context, keys ...string) bool
	Incr(ctx context.Context, key string, options ...Option) int64
	Close() error
}

type cacheRepo struct {
	client *redis.Client
}

cacheRepo — это прокси, который содержит частный объект redis.Client внутри, чтобы его можно было использовать вSet,GetПри ожидании удобно заняться обработкой отслеживания ссылок. Показать один здесьGetПримеры методов:

func (c *cacheRepo) Get(ctx context.Context, key string, options ...Option) (string, error) {
	var err error
	ts := time.Now()
	opt := newOption()
	defer func() {
		if opt.TraceRedis != nil {
			opt.TraceRedis.Timestamp = time_parse.CSTLayoutString()
			opt.TraceRedis.Handle = "get"
			opt.TraceRedis.Key = key
			opt.TraceRedis.CostSeconds = time.Since(ts).Seconds()
			opt.TraceRedis.Err = err

			addTracing(ctx, opt.TraceRedis)
		}
	}()

	for _, f := range options {
		f(opt)
	}

	value, err := c.client.Get(ctx, key).Result()
	if err != nil {
		err = werror.Wrapf(err, "redis get key: %s err", key)
	}
	return value, err
}
2. Промежуточное ПО

Промежуточное программное обеспечение здесь относится к расширению собственной http-инфраструктуры go для поддержки трехсторонней структуры цепочки методов запроса, такой как: gin, negroni и т. д., чтобы мы могли вставлять логику обработки до и после запроса, например : паника-восстановление, идентификация прав и т.д. Как упоминалось ранее, контекст отслеживания необходимо генерировать в начале запроса, и эта функция может быть выполнена в промежуточном программном обеспечении (если это микросервисная архитектура, она должна генерироваться на входном шлюзе). Сгенерированный контекст трассировки можно напрямую передать в журнал, чтобы журналы, напечатанные в последующей ссылке запроса, содержали идентификатор трассировки, что удобно для трассировки. В то же время требуемые индикаторы мониторинга (QPS, время отклика и т. д.) также могут быть выполнены вместе в этом промежуточном программном обеспечении.

4. Обработка ошибок

1. Ответ Обработка ошибок

Вообще говоря, наш интерфейс возвращает код ошибки для обработки некоторых ошибок бизнес-логики. Этот код ошибки отличается от кода состояния HTTP. Как правило, это таблица кодов ошибок, определяемая нами. Однако с точки зрения стандартизации нам все же необходимо обеспечить совместимость с некоторыми часто используемыми кодами состояния HTTP (400, 404, 500). при возвращении Подождите. Таким образом, нашей ошибке ответа нужны следующие возможности:

  1. Скрыть ошибки программы, особенно панику Программные ошибки, особенно панические, после того, как они были брошены, людям легко проанализировать детали внутренней реализации системы, поэтому обратите внимание на их сокрытие. В частности, многие веб-фреймворки автоматически восстанавливают панику, а затем распечатывают ее.
  2. Может легко определить код состояния HTTP, код ошибки Удобство здесь означает, что возвращаемый код состояния и код ошибки могут быть указаны на сервисном уровне, потому что только сервисный уровень может контролировать общую ситуацию.

Это очень просто реализовать, достаточно реализовать следующие пять методов:

// 根据状态码、错误码、错误描述创建一个Error
func NewError(httpCode, businessCode int, msg string) Error {}
// 状态码默认200,根据错误码、错误描述创建一个Error
func NewErrorWithStatusOk(businessCode int, msg string) Error {}
// 状态码默认200,根据错误码创建一个Error(错误描述从 错误码表 中获取)
func NewErrorWithStatusOkAutoMsg(businessCode int) Error {}
// 根据状态码、错误码创建一个Error
func NewErrorAutoMsg(httpCode, businessCode int) Error {}
// 把内部的err放到 Error 中
func (e *err) WithErr(err error) Error {}

Эта структура Error включает в себя код состояния, код ошибки, описание ошибки и реальную ошибку. При использовании пример кода выглядит следующим образом:

func (s *GoodsSvc) AddGoods(sctx core.SvcContext, param *model.GoodsAdd) error {
...
	if err != nil{
		return response.NewErrorAutoMsg(
			http.StatusInternalServerError,
			response.ServerError,
		).WithErr(err)
	}
2. Обработка ошибок в Go

Теперь, когда код ошибки написан, я должен упомянуть об обработке ошибок в Go.Обработка ошибок всегда была спорной частью Go.Что касается нашей повседневной разработки, есть три наиболее часто встречающиеся проблемы, и официальный Go официальный, и только один из них был решен в версии 1.13.

  1. Как ошибки распространяются через слои Поскольку, в ходе, на самом деле ошибка может содержать только строку, возможно, для Go Programmer добавит дополнительную информацию к ошибке и просто сделайте исходную ошибку. Нашей версии 1.13, черезfmt.ErrorfЭто требование выполняется только в том случае, если поддерживается пакетирование ошибок.
  2. Как получить стек в момент ошибки Но на самом деле в повседневной разработке есть еще один, который используется больше, а именно сбор стека при возникновении ошибки. Это отсутствие информации о стеке не очень удобно для новичков в Go. Рассмотрим две ситуации:
    func Set(key string, value string) error{
    	...
    	return err 
    }
    
    Общая функция Set, когда возникает ошибка и возвращается, если ее вызывающий объект верхнего уровня не обрабатывает ее должным образом, ошибка будет вызвана напрямую, что легко приведет к тому, что ошибка не может быть отслежена до источника (просто знайте, что ошибка выдается Сетом, но не знаю, кто звонил Сету). Второй случай — при внутренней обработке:
    func DoSomething(key string, value string) error{
    	...
    	err := io.Read()
    	if err != nil{
    		return fmt.Errors("Read: %w",err)
    	}
    	...  
    	err = io.Read()
    	if err != nil{
    		return fmt.Errors("Read2: %w",err)
    	}
    }
    
    Как видите, в функции вызывается несколько функций (например, вызов Read для чтения файла), чтобы отличить err, мы должны обернуть каждую ошибку, иначе вы не знаете, на какой Read выдает проблему. все. Тем не менее, писать такой код по-прежнему немного неприятно, и у самого Go нет хорошего способа сделать это. Сообщество с открытым исходным кодом предоставляет множество пакетов с ошибками, таких как:github.com/pkg/errors, который используется для добавления ошибки в стек, чтобы легко решить вышеуказанные проблемы. К сожалению, хотя обработка ошибок версии 1.13 относится к реализации с открытым исходным кодом, она не включает функцию стека.
  3. Как объединить несколько ошибок В некоторых особых сценариях, например, в серии логики обработки, хотя ошибка возникает, но логика не прерывается, а просто требуется записать ошибку, а затем продолжить. Например: Существует требование, чтобы статистическая система получала доступ к системам заказов, товаров и пользователей каждую ночь и извлекала некоторые статистические данные из каждой системы. Мы можем установить в системе статистики временную задачу, чтобы пуллировать каждую ночь, когда пулл терпит неудачу, записываем ошибку и продолжаем пулить следующий.
	func StartPull(){
		var errMulti error
		for i := range systems{
			if err := Pull(systems[i]); err != nil{
				errMulti = multierror.Append(errMulti, err)
			}
		}
	}

Здесь мы используемgithub.com/hashicorp/go-multierrorЭтот библиотечный процесс на самом деле представляет собой массив []ошибок. Здесь следует отметить разницу между агрегированием нескольких ошибок и обработкой ошибок.При агрегировании ошибок не может быть связи между несколькими ошибками, обвязка ошибок имеет ассоциации верхнего и нижнего уровня.

5. Обработка слоя дао

Разделение слоя c/s/d и роль каждого слоя упомянуты выше.Здесь я хочу подробно рассказать о слое дао.

1. Автоматически генерировать код

На самом деле, если вы напишете больше кода уровня dao, вы обнаружите, что многие коды являются обобщенными, например, код CURD. Это не что иное, как изменение таблицы, а логика та же. Такой код очень подходит для автоматической генерации с помощью генераторов, таких как:gormtОперация CURD в gorm может быть сгенерирована автоматически, и на ней очень легко написать несколько небольших систем.

2. Скрытие поля

Если вы напрямую пишете имя таблицы базы данных и имя поля в коде, это определенно не очень хорошая практика. Как правило, нам приходится использовать некоторые структуры и константы вместо того, чтобы писать их прямо в коде. В связи с этим инструменты также могут быть использованы для автоматической генерации без ручного написания. Например:gormt,gopoЖдать. С автоматически сгенерированными именами полей, а также мощнымGORM V2Framework, наш уровень dao может упростить функции, необходимые для верхнего уровня.

func FindBy(id int, columns ...string){
    db.Select(colums).Find(&SomeTable{},id)
}
// 只取 ID Name 字段
bean := FindBy(1,dao.Colums.ID, dao.Colums.Name)
// 只取 Status 字段
bean := FindBy(2,dao.Colums.Status)

Образец кода дает простой пример, черезFindByфункция, вызов верхнего уровня может свободно управлять полями, которые он хочет получить, и ему не нужно раскрывать имена полей базовой базы данных. заCreate,Updateи т. д., также может быть достигнут такой же эффект управления, который здесь повторяться не будет.

3. Обновите поля

Обновления полей являются проблематичным моментом из-за спецификации go с нулевым значением (0\false""). (часто пишу баги по этому поводу) Логически говоря, предоставляемая нами функция обновления должна соответствовать следующим пунктам:

  1. Нулевое значение базы данных соответствует нулевому значению go При проектировании базы данных обычно рекомендуется не использовать поле NULL, а вместо этого использовать значение по умолчанию.Это значение по умолчанию обычно является нулевым значением типа, который соответствует go.
  2. Обновление по требованию Имеется в виду дизайн интерфейса, который предусматривает, что входящее поле должно быть полем для обновления, а поле, которое не нужно обновлять, не должно появляться. Например:
    // PUT /score
    {
    	"id": 1,
    	"name": "张三",
    	"score": 100,
    	"create_time": "2021-12-12"
    }
    
    Через приведенный выше json мы создаем часть данных, как в это время спроектировать интерфейс обновления?
    // POST /score
    {	
    	"id": 1,
    	"name": "不对,我叫李四",
    	"score": 0,
    	"create_time": ""
    }
    
    Как и выше, если указано, что поле с нулевым значением не обновляется, в обновлении участвует только поле с ненулевым значением. Итак, если пользователь действительно хочет поставитьscoreЧто делать, когда поле обновляется до 0, на самом деле нулевое значение и нулевое значение создают неоднозначность.
    // POST /score
    {
    	"id": 1,
    	"name": "我叫李四"
    }
    
    Итак, чтобы избежать двусмысленности, мы оговариваем, что поля, которые не обновляются, не должны появляться в json. Точно так же, когда json преобразуется в структуру в go, мы можем получить его только через указатель:
    type UpdateScore struct{
    	Id int 
    	Name *string
    	Score *int
    	CreateTime *string
    }
    
    Без получения указателя вы все равно не сможете судить, равен ли Score в структуре 0, действительно ли он установлен в 0 или не передается в json. Хотя с указателем смотреть немного сложно, но никак, конфликт между нулевыми значениями и нулевыми значениями можно решить только таким образом. Либо вместо указателей указываю, что все поля интерфейса обновления должны передаваться, и все обновляются в БД (или считываются исходные данные из БД, и обновляются измененные поля).
    // POST /score
    {	
    	"id": 1,
    	"name": "不对,我叫李四",
    	"score": 100,
    	"create_time": "2021-12-12"
    }
    
    Но таким образом, вы должны прочитать данные один раз для сравнения, и обновленный json станет очень большим, и будет передано много избыточных данных, что лично я считаю еще более невыполнимым.

6. Приложение

Хоть я и обобщил 5 пунктов, думаю, что мелких моментов знаний еще много, и другие аспекты могут быть не всем, что можно написать в статье. Если он встречается или встречается позже, он будет продолжать обобщать. Ведь древние говорили: «Учиться и учиться время от времени, это не то, что говорить». Без резюме вы не сможете увидеть полную картину проблемы. Прилагаю некоторые ссылки:

  • shopПример проекта, который я создал, — это полностью работающий товарный сервис. Многие из примеров кода в этой статье преобразованы из этого проекта, потому что код в нем имел дело с проблемами, упомянутыми выше.
  • go-gin-apiблагодарныйxinliangnoteПосле тщательного изучения проекта я чувствую, что он меня очень вдохновил. Отсюда можно узнать много кода и модулей.
  • GoFrame Хотя GoFrame немного великоват, нельзя отрицать, что код и документация в нем очень систематизированы и информативны. Что касается структуры кода, то она также имеет очень подробную интерпретацию.
  • KratosЭта статья о Wire в фреймворке kratos относительно проста для понимания. (относительно официальной документации провода)
  • Отслеживание ссылок GoFrameGoFrame по-прежнему рекомендуется, и отслеживание ссылок также очень хорошо.