golang

Сравнение кода от DeepSeek и ChatGPT по-гоферски

  • понедельник, 3 февраля 2025 г. в 00:00:14
https://habr.com/ru/articles/878758/

В поисках утреннего чтива на Хабре наткнулся на статью про сравнение кода на Swift, сгенерированного двумя действующими лицами - ChatGPT от OpenAI и его нашумевший "убийца" - DeepSeek, основателем которого выступает Лян Вэньфэн, про которого можно узнать в статье на VC.

После прочтения статьи в голову вполне ожидаемо пришла идея сравнения сгенерированного кода на Go. Правила те же самые - использование одинаковых промптов для каждой модели. Если модель выдает неработоспособный результат - даем еще одну попытку, но условно ставим "незачет".

В статье автор сравнивал устаревшие версии GPT 4o и DeepSeek V3. Здесь же будет сравнение более актуальных моделей - GPT o1 и DeepSeek R1 (DeepThink).

Сравнивать результаты буду по следующим критериям:

  1. Работоспособность - 1 балл. Присваиваем, если код успешно компилируется и запускается.

  2. Выполнение требований по задаче - по 1 баллу за каждое требование.

  3. Когнитивная сложность - 1 балл. Моя субъективная оценка сложности восприятия кода, достается только одному.

  4. Документирование кода - 2 балла. 1 балл за комментарии к функциям, 1 балл за комментарии к определенным кускам кода.

  5. Пример для запуска - 1 балл. Условно это должна быть функция main, которая содержит готовые данные для проверки работоспособности кода (кроме змейки, она и так должна быть исполняемой программой).

  6. Дизайн змейки - 1 балл. Моя субъективная оценка.

  7. Реализация сложности змейки - 1 балл. Моя субъективная оценка. Отличается от сложности в требованиях тем, что балл дается за качество реализации сложности.

Система баллов оставляет желать лучшего, но у меня не было желания для более тщательной проработки.

Начнем мы так же, как и во вдохновившей меня статье - всеми любимой "змейки". Для генерации был использован такой промпт:

Напиши реализацию простого варианта игры "змейка" на языке Go для запуска в терминале. Змейка должна быть зеленого цвета, еда - красного. Поле должно иметь границы 15 на 15. Должен отображаться счетчик еды. Перед началом игры должна быть возможность выбрать сложность игры, от которого будет зависеть скорость змейки - низкая, средняя и высокая. Управление змейкой должно осуществляться стрелками на клавиатуре. Из внешних зависимостей может быть использована только библиотека https://github.com/charmbracelet/bubbletea.

Соберем требования в список:

  1. Змейка зеленого цвета, еда красного.

  2. Границы 15 на 15 символов.

  3. Счетчик съедненной еды.

  4. Реализация разных версий сложности игры.

  5. Управление стрелками.

Вот что нам сгенерировал ChatGPT o1:

Код от ChatGPT
package main

import (
	"fmt"
	"math/rand"
	"time"

	bubbletea "github.com/charmbracelet/bubbletea"
)

// Размер игрового поля.
const (
	boardWidth  = 15
	boardHeight = 15
)

// Возможные состояния приложения: меню выбора сложности, игра, конец игры.
type gameState int

const (
	stateMenu gameState = iota
	statePlaying
	stateGameOver
)

// Возможные направления движения змейки.
type direction int

const (
	dirUp direction = iota
	dirDown
	dirLeft
	dirRight
)

// Сложность и соответствующие интервалы тиков.
var (
	difficulties = []string{"Низкая", "Средняя", "Высокая"}
	speeds       = []time.Duration{
		300 * time.Millisecond, // Низкая
		150 * time.Millisecond, // Средняя
		75 * time.Millisecond,  // Высокая
	}
)

// Сообщения, используемые в Update.
type tickMsg time.Time
type gameOverMsg struct{}
type restartMsg struct{}

// Позиция на поле.
type position struct {
	x int
	y int
}

// Модель приложения.
type model struct {
	state gameState

	// --- Меню ---
	menuChoice int // Индекс выбранной сложности

	// --- Игра ---
	snake      []position // Срез координат змейки
	direction  direction  // Текущее направление движения
	food       position   // Позиция еды
	score      int        // Количество съеденной еды
	speed      time.Duration
	randSource rand.Source // Для генерации случайных позиций еды
}

// Обязательный метод Init() для реализации интерфейса bubbletea.Model.
func (m model) Init() bubbletea.Cmd {
	// На старте ничего не делаем. Можно вернуть nil.
	return nil
}

// Инициализация модели (начальное состояние).
func initialModel() model {
	m := model{
		state:      stateMenu,
		menuChoice: 0,
	}
	return m
}

// Команда тика, чтобы игра обновлялась через определённые промежутки.
func tickCmd(d time.Duration) bubbletea.Cmd {
	return bubbletea.Tick(d, func(t time.Time) bubbletea.Msg {
		return tickMsg(t)
	})
}

// Инициализация новой игры (при переходе из меню или рестарте).
func (m *model) initGame() {
	// Сразу зададим начальное направление — вправо.
	m.direction = dirRight
	m.score = 0

	// Пусть начальная длина змейки = 1, позиционируем змею в центре.
	startPos := position{
		x: boardWidth / 2,
		y: boardHeight / 2,
	}
	m.snake = []position{startPos}

	// Генератор случайных чисел.
	m.randSource = rand.NewSource(time.Now().UnixNano())

	// Сгенерировать еду в случайном месте.
	m.generateFood()
}

// Генерация еды в новом месте, где нет змейки.
func (m *model) generateFood() {
	rnd := rand.New(m.randSource)
	for {
		foodPos := position{
			x: rnd.Intn(boardWidth),
			y: rnd.Intn(boardHeight),
		}
		// Проверим, чтобы еда не попадала на змею.
		if !m.containsSnake(foodPos) {
			m.food = foodPos
			return
		}
	}
}

// Проверка, содержится ли позиция внутри змейки.
func (m *model) containsSnake(pos position) bool {
	for _, s := range m.snake {
		if s.x == pos.x && s.y == pos.y {
			return true
		}
	}
	return false
}

// Логика обновления модели по сообщениям.
func (m model) Update(msg bubbletea.Msg) (bubbletea.Model, bubbletea.Cmd) {
	switch m.state {
	// ------------------------------------------------
	// Меню выбора сложности
	// ------------------------------------------------
	case stateMenu:
		switch msg := msg.(type) {
		case bubbletea.KeyMsg:
			switch msg.String() {
			case "up":
				if m.menuChoice > 0 {
					m.menuChoice--
				}
			case "down":
				if m.menuChoice < len(difficulties)-1 {
					m.menuChoice++
				}
			case "enter":
				// Устанавливаем скорость в зависимости от выбранной сложности.
				m.speed = speeds[m.menuChoice]
				m.initGame()
				m.state = statePlaying
				// Запускаем тики.
				return m, tickCmd(m.speed)
			case "q", "esc":
				// Выход из игры.
				return m, bubbletea.Quit
			}
		}
		return m, nil

	// ------------------------------------------------
	// Игровое состояние
	// ------------------------------------------------
	case statePlaying:
		switch msg := msg.(type) {
		// Каждый тик — сдвиг змейки
		case tickMsg:
			m = m.moveSnake()
			// Если после движения мы в состоянии Game Over, значит столкнулись.
			if m.state == stateGameOver {
				return m, nil
			}
			// Иначе продолжаем тики.
			return m, tickCmd(m.speed)

		case bubbletea.KeyMsg:
			switch msg.String() {
			case "up":
				if m.direction != dirDown { // Чтобы избежать разворота на 180
					m.direction = dirUp
				}
			case "down":
				if m.direction != dirUp {
					m.direction = dirDown
				}
			case "left":
				if m.direction != dirRight {
					m.direction = dirLeft
				}
			case "right":
				if m.direction != dirLeft {
					m.direction = dirRight
				}
			case "q", "esc":
				// Выход из игры.
				return m, bubbletea.Quit
			}
		}
		return m, nil

	// ------------------------------------------------
	// Состояние конца игры
	// ------------------------------------------------
	case stateGameOver:
		switch msg := msg.(type) {
		case bubbletea.KeyMsg:
			switch msg.String() {
			case "r":
				// Перезапуск: возвращаемся в меню.
				m.state = stateMenu
				m.menuChoice = 0
				return m, nil
			case "q", "esc":
				return m, bubbletea.Quit
			}
		}
		return m, nil
	}

	return m, nil
}

// Функция перемещения змейки.
func (m model) moveSnake() model {
	head := m.snake[len(m.snake)-1]
	newHead := head

	// Определяем новый заголовок (координаты головы) по направлению.
	switch m.direction {
	case dirUp:
		newHead.y--
	case dirDown:
		newHead.y++
	case dirLeft:
		newHead.x--
	case dirRight:
		newHead.x++
	}

	// Проверяем выход за границы — конец игры.
	if newHead.x < 0 || newHead.x >= boardWidth ||
		newHead.y < 0 || newHead.y >= boardHeight {
		m.state = stateGameOver
		return m
	}

	// Проверяем столкновение с самим собой.
	if m.containsSnake(newHead) {
		m.state = stateGameOver
		return m
	}

	// Если попали на еду.
	if newHead.x == m.food.x && newHead.y == m.food.y {
		// Увеличиваем счёт.
		m.score++
		// Добавляем новую голову, хвост не удаляем (змейка растёт).
		m.snake = append(m.snake, newHead)
		// Генерируем новую еду.
		m.generateFood()
	} else {
		// Сдвигаем хвост и добавляем новую голову.
		m.snake = append(m.snake[1:], newHead)
	}

	return m
}

// Отрисовка меню, игрового поля и экрана конца игры.
func (m model) View() string {
	switch m.state {
	case stateMenu:
		return m.viewMenu()
	case statePlaying:
		return m.viewGame()
	case stateGameOver:
		return m.viewGameOver()
	default:
		return ""
	}
}

// Отображение меню выбора сложности.
func (m model) viewMenu() string {
	s := "Выберите сложность:\n\n"
	for i, diff := range difficulties {
		cursor := "  "
		if i == m.menuChoice {
			cursor = "> "
		}
		s += fmt.Sprintf("%s%s\n", cursor, diff)
	}
	s += "\n↑/↓ - выбор, Enter - подтвердить, q/esc - выход\n"
	return s
}

// Отображение игрового поля.
func (m model) viewGame() string {
	var view string

	// Счёт.
	view += fmt.Sprintf("Счёт: %d\n\n", m.score)

	// Создадим карту для быстрой проверки: где находится змейка?
	snakePositions := make(map[position]bool)
	for _, pos := range m.snake {
		snakePositions[pos] = true
	}

	// Рисуем сетку 15x15.
	for y := 0; y < boardHeight; y++ {
		for x := 0; x < boardWidth; x++ {
			switch {
			case x == m.food.x && y == m.food.y:
				// Красная еда
				view += "\033[31m*\033[0m"
			case snakePositions[position{x, y}]:
				// Змейка (зелёная)
				view += "\033[32mO\033[0m"
			default:
				view += " "
			}
		}
		view += "\n"
	}

	view += "\nНажмите q/esc для выхода.\n"
	return view
}

// Отображение экрана конца игры.
func (m model) viewGameOver() string {
	s := fmt.Sprintf("Игра окончена! Ваш счёт: %d\n", m.score)
	s += "Нажмите r для возврата в меню или q/esc для выхода.\n"
	return s
}

func main() {
	rand.Seed(time.Now().UnixNano())
	p := bubbletea.NewProgram(initialModel())
	if err := p.Start(); err != nil {
		fmt.Println("Ошибка запуска программы:", err)
	}
}

Змейка от GPT o1
Змейка от GPT o1

Сразу бросается в глаза отсутствие видимых границ (но они есть). Это можно оправдать уровнем "хардкор", но в требованиях такого не было (как и требования визуализации границ, однако).

Итого, GPT o1 получает за змейку 4 (по требованиям) + 1 (работоспособность) + 1 (когнитивная сложность) + 2 (документирование) + 1 (реализация сложности) = 9 баллов. За дизайн не стал добавлять балл из-за отсутствия визуализации границ.

А вот что получилось у DeepSeek R1:

Код от DeepSeek
package main

import (
	"fmt"
	"math/rand"
	"os"
	"time"

	bubbletea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"golang.org/x/term"
)

const (
	width  = 15
	height = 15
	border = 1
)

type direction int

const (
	up direction = iota
	down
	left
	right
)

type difficulty int

const (
	low difficulty = iota
	medium
	high
)

type model struct {
	snake      []position
	food       position
	dir        direction
	nextDir    direction
	score      int
	gameOver   bool
	difficulty difficulty
	chosenDiff bool
}

type position struct {
	x, y int
}

type tickMsg time.Time

var (
	snakeStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
	foodStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
	borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
	scoreStyle  = lipgloss.NewStyle().Bold(true)
)

func initialModel() model {
	return model{
		snake: []position{{x: width/2 + border, y: height/2 + border}},
		dir:   right,
	}
}

func (m model) Init() bubbletea.Cmd {
	return tick()
}

func tick() bubbletea.Cmd {
	return bubbletea.Tick(time.Second, func(t time.Time) bubbletea.Msg {
		return tickMsg(t)
	})
}

func (m model) Update(msg bubbletea.Msg) (bubbletea.Model, bubbletea.Cmd) {
	if !m.chosenDiff {
		switch msg := msg.(type) {
		case bubbletea.KeyMsg:
			switch msg.String() {
			case "1":
				m.difficulty = low
				m.chosenDiff = true
			case "2":
				m.difficulty = medium
				m.chosenDiff = true
			case "3":
				m.difficulty = high
				m.chosenDiff = true
			case "q", "ctrl+c":
				os.Exit(0)
			}
		}
		if m.chosenDiff {
			m.placeFood()
			return m, bubbletea.Tick(getInterval(m.difficulty), func(t time.Time) bubbletea.Msg {
				return tickMsg(t)
			})
		}
		return m, nil
	}

	if m.gameOver {
		if key, ok := msg.(bubbletea.KeyMsg); ok {
			if key.String() == "q" || key.String() == "ctrl+c" {
				os.Exit(0)
			}
		}
		return m, nil
	}

	switch msg := msg.(type) {
	case bubbletea.KeyMsg:
		switch msg.String() {
		case "up":
			if m.dir != down {
				m.nextDir = up
			}
		case "down":
			if m.dir != up {
				m.nextDir = down
			}
		case "left":
			if m.dir != right {
				m.nextDir = left
			}
		case "right":
			if m.dir != left {
				m.nextDir = right
			}
		case "q", "ctrl+c":
			os.Exit(0)
		}

	case tickMsg:
		m.dir = m.nextDir
		head := m.snake[0]
		newHead := head

		switch m.dir {
		case up:
			newHead.y--
		case down:
			newHead.y++
		case left:
			newHead.x--
		case right:
			newHead.x++
		}

		if newHead.x <= border || newHead.x >= width+border ||
			newHead.y <= border || newHead.y >= height+border ||
			m.collidesWithSnake(newHead) {
			m.gameOver = true
			return m, nil
		}

		m.snake = append([]position{newHead}, m.snake...)

		if newHead == m.food {
			m.score++
			m.placeFood()
		} else {
			m.snake = m.snake[:len(m.snake)-1]
		}

		return m, bubbletea.Tick(getInterval(m.difficulty), func(t time.Time) bubbletea.Msg {
			return tickMsg(t)
		})
	}

	return m, nil
}

func (m *model) placeFood() {
	for {
		m.food = position{
			x: rand.Intn(width) + border + 1,
			y: rand.Intn(height) + border + 1,
		}
		if !m.collidesWithSnake(m.food) {
			break
		}
	}
}

func (m model) collidesWithSnake(p position) bool {
	for _, segment := range m.snake {
		if segment == p {
			return true
		}
	}
	return false
}

func getInterval(d difficulty) time.Duration {
	switch d {
	case low:
		return 200 * time.Millisecond
	case medium:
		return 100 * time.Millisecond
	case high:
		return 50 * time.Millisecond
	default:
		return 100 * time.Millisecond
	}
}

func (m model) View() string {
	if !m.chosenDiff {
		return "Choose difficulty:\n1. Low\n2. Medium\n3. High\n\nPress q to quit"
	}

	if m.gameOver {
		return fmt.Sprintf("Game Over! Score: %d\nPress q to quit", m.score)
	}

	physicalWidth, physicalHeight, _ := term.GetSize(int(os.Stdout.Fd()))
	field := m.drawField()
	score := scoreStyle.Render(fmt.Sprintf("Score: %d", m.score))

	return lipgloss.Place(
		physicalWidth,
		physicalHeight,
		lipgloss.Center,
		lipgloss.Center,
		lipgloss.JoinVertical(lipgloss.Center, field, score),
	)
}

func (m model) drawField() string {
	field := ""
	for y := 0; y < height+border*2+1; y++ {
		for x := 0; x < width+border*2+1; x++ {
			switch {
			case x == border || x == width+border || y == border || y == height+border:
				field += borderStyle.Render("#")
			case x == m.food.x && y == m.food.y:
				field += foodStyle.Render("●")
			default:
				isSnake := false
				for i, p := range m.snake {
					if p.x == x && p.y == y {
						isSnake = true
						if i == 0 {
							field += snakeStyle.Render("●")
						} else {
							field += snakeStyle.Render("○")
						}
						break
					}
				}
				if !isSnake {
					field += " "
				}
			}
		}
		field += "\n"
	}
	return field
}

func main() {
	rand.Seed(time.Now().UnixNano())
	p := bubbletea.NewProgram(initialModel(), bubbletea.WithAltScreen())
	if err := p.Start(); err != nil {
		fmt.Println("Error running program:", err)
		os.Exit(1)
	}
}

Визуально змейка из шариков с отличающейся "головой" от DeepSeek выглядит гораздо приятнее. Но вот реализация сложности подвела.

Итого, DeepSeek R1 зарабатывает 4 (по требованиям) + 1 (работоспособность) + 1 (дизайн змейки) = 6 баллов.

Меньше чем получил GPT, но считаю оценку справделивой. Реализация сложности ужасная, я просто не успеваю среагировать и понять где сама змейка, а игра уже закончена. Но тем не менее выбор сложности присутствует, и я решил все-таки добавить балл за это. Комментариев в коде нет от слова совсем, и код сложнее читать когда уровень вложенности достигает цифры 6. Требование по границам тоже несоблюдено (у DeepSeek больше 15 символов), хотя сейчас кажется, что я перегнул с сеткой 15 на 15, надо было указывать больше.


Следующим испытанием будет реализация worker pool с приоритетами. Задачи для пула будут разделены на два типа по приоритетности - LowPriority и HighPriority. Промт следующий:

Напиши реализацию worker pool с приоритетами на языке Go. Необходимо учесть следующие требования: 1. Количество воркеров и размер каналов для задач должно задаваться пользователем. 2. Задачи должны иметь поле с типом приоритета - HighPriority и LowPriority. 3. Перед завершением нужно гарантировать обработку всех задач с приоритетом HighPriority.

Снова собираем требования в список для удобства оценки:

  1. Колчичество воркеров и размер буфера каналов задается пользователем.

  2. Задачи разделены на два типа приоритетности - HighPriority и LowPriority.

  3. Перед завершением программы нужно гарантировать выполнение всех задач с высоким приоритетом.

Реализация от ChatGPT o1:

Worker pool от ChatGPT
package main

import (
	"fmt"
	"sync"
	"time"
)

// Priority определяет приоритет задачи.
type Priority int

const (
	LowPriority Priority = iota
	HighPriority
)

// Task описывает задачу для выполнения.
type Task struct {
	Priority Priority
	Job      func()
}

// WorkerPool управляет пулам воркеров с приоритетом задач.
type WorkerPool struct {
	highPriorityCh chan Task
	lowPriorityCh  chan Task

	// Количество воркеров
	workerCount int

	// Канал для сигнализации остановки
	stopCh chan struct{}

	// wgHigh отслеживает только задачи с высоким приоритетом
	wgHigh sync.WaitGroup

	// wgAll отслеживает все задачи (если захотите дождаться вообще всех)
	wgAll sync.WaitGroup

	// Один раз закрываем stopCh
	once sync.Once
}

// NewWorkerPool создаёт новый пул воркеров.
//   - workerCount: количество воркеров
//   - queueSize: размер каналов для задач
func NewWorkerPool(workerCount, queueSize int) *WorkerPool {
	return &WorkerPool{
		highPriorityCh: make(chan Task, queueSize),
		lowPriorityCh:  make(chan Task, queueSize),
		workerCount:    workerCount,
		stopCh:         make(chan struct{}),
	}
}

// Start запускает заданное количество горутин-воркеров.
func (p *WorkerPool) Start() {
	for i := 0; i < p.workerCount; i++ {
		go p.worker()
	}
}

// Submit добавляет задачу в очередь соответствующего приоритета.
// Если каналы заполнены, вызов будет блокироваться до освобождения места.
func (p *WorkerPool) Submit(task Task) {
	// Увеличиваем счетчики в зависимости от приоритета
	p.wgAll.Add(1)
	if task.Priority == HighPriority {
		p.wgHigh.Add(1)
	}

	// Отправляем задачу в нужный канал
	switch task.Priority {
	case HighPriority:
		p.highPriorityCh <- task
	case LowPriority:
		p.lowPriorityCh <- task
	}
}

// Stop останавливает пул воркеров:
//   1. Дожидаемся выполнения всех задач с высоким приоритетом.
//   2. Закрываем канал stopCh, чтобы сигнализировать воркерам завершение работы.
// Обратите внимание: если вам нужно дождаться всех задач (включая LowPriority),
// то дополнительно вызовите p.wgAll.Wait().
func (p *WorkerPool) Stop() {
	// Сначала дожидаемся выполнения всех высокоприоритетных задач
	p.wgHigh.Wait()

	// Сигнализируем горутинам, что пора завершаться
	p.once.Do(func() {
		close(p.stopCh)
	})

	// Если хотите гарантировать завершение ВСЕХ задач, то раскомментируйте:
	// p.wgAll.Wait()
}

// собственно worker-функция, которую исполняют горутины
func (p *WorkerPool) worker() {
	for {
		select {
		case <-p.stopCh:
			// Как только получили сигнал остановки – выходим
			return
		default:
		}

		// Сначала пытаемся взять задачу из highPriorityCh (если там что-то есть)
		select {
		case task := <-p.highPriorityCh:
			p.execute(task)
		default:
			// Если в highPriorityCh нет задач, берём из lowPriorityCh
			select {
			case task := <-p.lowPriorityCh:
				p.execute(task)
			case <-p.stopCh:
				return
			}
		}
	}
}

// execute выполняет переданную задачу и корректно обновляет счётчики.
func (p *WorkerPool) execute(task Task) {
	defer func() {
		// По завершении задачи уменьшаем оба счётчика (при необходимости)
		p.wgAll.Done()
		if task.Priority == HighPriority {
			p.wgHigh.Done()
		}
	}()
	// Собственно выполнение задачи
	task.Job()
}

// Пример использования
func main() {
	// Создаём пул с 3 воркерами и размером каналов 10
	pool := NewWorkerPool(3, 10)

	// Запускаем воркеров
	pool.Start()

	for i := 0; i < 5; i++ {
		// Задачи с низким приоритетом
		pool.Submit(Task{
			Priority: LowPriority,
			Job: func() {
				fmt.Println("Low priority task started")
				time.Sleep(time.Millisecond * 500)
				fmt.Println("Low priority task finished")
			},
		})
	}

	// Публикуем несколько задач в пул
	for i := 0; i < 5; i++ {
		// Задачи с высоким приоритетом
		pool.Submit(Task{
			Priority: HighPriority,
			Job: func() {
				fmt.Println("High priority task started")
				time.Sleep(time.Millisecond * 200)
				fmt.Println("High priority task finished")
			},
		})
	}

	// Останавливаем пул. Гарантируется, что все HighPriority-задачи закончатся.
	pool.Stop()

	// Если нужно дождаться завершения всех задач, раскомментируйте:
	// pool.wgAll.Wait()

	fmt.Println("Main finished")
}

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

Расстановка приоритетов от GPT o1
Расстановка приоритетов от GPT o1

Оцениваем: сейчас у GPT o1 9 баллов + 3 (за выполнение требований) + 1 (работоспособность) + 1 (когнитивная сложность) + 2 (документирование) + 1 (пример для запуска) = 17 баллов.

А теперь посмотрим на ответ DeepSeek:

Worker pool от DeepSeek
package main

import (
	"errors"
	"fmt"
	"sync"
	"time"
)

type Priority int

const (
	HighPriority Priority = iota
	LowPriority
)

type Task struct {
	Priority Priority
	Job      func()
}

type WorkerPool struct {
	highChan chan Task
	lowChan  chan Task
	highWg   sync.WaitGroup
	lowWg    sync.WaitGroup
	quit     chan struct{}
	once     sync.Once
}

func NewWorkerPool(numWorkers, highChanSize, lowChanSize int) *WorkerPool {
	wp := &WorkerPool{
		highChan: make(chan Task, highChanSize),
		lowChan:  make(chan Task, lowChanSize),
		quit:     make(chan struct{}),
	}

	for i := 0; i < numWorkers; i++ {
		go wp.worker()
	}

	return wp
}

func (wp *WorkerPool) Submit(task Task) error {
	select {
	case <-wp.quit:
		return errors.New("worker pool is stopped")
	default:
	}

	switch task.Priority {
	case HighPriority:
		select {
		case wp.highChan <- task:
			wp.highWg.Add(1)
		default:
			return errors.New("high priority queue is full")
		}
	case LowPriority:
		select {
		case wp.lowChan <- task:
			wp.lowWg.Add(1)
		default:
			return errors.New("low priority queue is full")
		}
	default:
		return errors.New("invalid task priority")
	}

	return nil
}

func (wp *WorkerPool) Stop() {
	wp.once.Do(func() {
		close(wp.quit)
		close(wp.highChan)
		close(wp.lowChan)
		wp.highWg.Wait()
		wp.lowWg.Wait()
	})
}

func (wp *WorkerPool) worker() {
	defer func() {
		// Process remaining high priority tasks first
		for task := range wp.highChan {
			task.Job()
			wp.highWg.Done()
		}
		// Then process low priority tasks
		for task := range wp.lowChan {
			task.Job()
			wp.lowWg.Done()
		}
	}()

	for {
		select {
		case <-wp.quit:
			return
		default:
		}

		select {
		case task, ok := <-wp.highChan:
			if !ok {
				return
			}
			task.Job()
			wp.highWg.Done()
		default:
			select {
			case task, ok := <-wp.highChan:
				if !ok {
					return
				}
				task.Job()
				wp.highWg.Done()
			case task, ok := <-wp.lowChan:
				if !ok {
					return
				}
				task.Job()
				wp.lowWg.Done()
			}
		}
	}
}

И тут сразу видим отсутствие main-функции с примером для запуска и тестирования этой реализации, поэтому дописываем его самостоятельно по аналогии с кодом от GPT o1:

func main() {
	// Создаём пул с 3 воркерами и размером каналов 10
	pool := NewWorkerPool(3, 10, 10)

	for i := 0; i < 5; i++ {
		// Задачи с низким приоритетом
		_ = pool.Submit(Task{
			Priority: LowPriority,
			Job: func() {
				fmt.Println("Low priority task started")
				time.Sleep(time.Millisecond * 500)
				fmt.Println("Low priority task finished")
			},
		})
	}

	// Публикуем несколько задач в пул
	for i := 0; i < 5; i++ {
		// Задачи с высоким приоритетом
		_ = pool.Submit(Task{
			Priority: HighPriority,
			Job: func() {
				fmt.Println("High priority task started")
				time.Sleep(time.Millisecond * 200)
				fmt.Println("High priority task finished")
			},
		})
	}

	// Останавливаем пул
	pool.Stop()

	fmt.Println("Main finished")
}

В коде сразу видно одну проблему - функция Stop() ждет выполнения горутин, которые обрабатывают задачи с низким приоритетом:

func (wp *WorkerPool) Stop() {
	wp.once.Do(func() {
		close(wp.quit)
		close(wp.highChan)
		close(wp.lowChan)
		wp.highWg.Wait()
		wp.lowWg.Wait()
	})
}

Пробуем запустить пул воркеров от DeepSeek с надеждой, что я ошибся, но получаем лишь подтверджение этой проблемы. Да, решается это очень просто - комментированием строки wp.lowWg.Wait() , чтобы не ждать выполнения горутин, которые получили задачи с низким приоритетом. Но GPT o1 не просто реализовал все корректно, но еще добавил дополнительную закомментированную строку с пояснением:

// Если нужно дождаться завершения всех задач, раскомментируйте: 
// pool.wgAll.Wait()
Подтверждаем ошибку от DeepSeek
Подтверждаем ошибку от DeepSeek

Подводим итоги для DeepSeek R1: сейчас у него 6 баллов + 2 (за выполнение требований) + 1 (работоспособность) = 9 баллов. Снова нет баллов за документирование (комментариев нет вообще), отсутствовал пример для запуска и балл за когнитивную сложность решил отдать GPT o1.


Последней задачей будет реализация простого веб-сервера для CRUD-операций над задачами.

Промт составил следующий:

Напиши простой веб-сервер на Go, который позволяет выполнять CRUD-операции над сущностью "Задача (Task)". Используй только стандартную библиотеку Go, сторонние пакеты запрещены. Эндпоинты должны иметь вид `api/v1/task`. В качестве хранилища используй стандартный тип map.

Да, здесь описано мало требований, таких как разделение эндпоинтов по методам (POST для создания, PUT для обновления и так далее), но это сделано специально, чтобы в результатах было больше "отсебятины".

А наши требование таковы:

  1. Обязательная реализация 4-х методов - для получения, создания, обновления и удаления задач.

  2. Хранилище (в нашем случае map) должно быть потокобезопасным.

  3. Дополнительный балл за разделение операций "по REST" - то есть использование одного эндпоинта с разными HTTP-методами.

  4. Дополнительный балл за использование корректных HTTP-статусов в зависимости от природы ошибки.

  5. Дополнительный балл за логирование запросов в любом виде (не было в требованиях промпта).

  6. Дополнительный балл за автоматическую генерацию идентификаторов задач (не было в требованиях промпта).

Посмотрим на реализацию GPT o1:

Реализация CRUD от ChatGPT
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
)

// Task — наша сущность "Задача".
type Task struct {
	ID          string `json:"id"`
	Title       string `json:"title"`
	Description string `json:"description"`
}

// tasksMap — хранилище наших задач в памяти.
var tasksMap = make(map[string]Task)

// mutex — простой мьютекс для безопасного доступа к карте из нескольких горутин.
var mutex = &sync.Mutex{}

func main() {
	http.HandleFunc("/api/v1/task/", taskHandler)
	http.HandleFunc("/api/v1/task", taskHandler)

	fmt.Println("Сервер запущен на порту 8080...")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

// taskHandler обрабатывает все CRUD-операции над задачами.
func taskHandler(w http.ResponseWriter, r *http.Request) {
	// Отключаем кэширование и устанавливаем тип контента как JSON.
	w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
	w.Header().Set("Content-Type", "application/json; charset=utf-8")

	// Определяем, есть ли в пути ID задачи (например, /api/v1/task/123).
	// Если путь заканчивается на /api/v1/task/, значит ID либо отсутствует, либо пустой.
	id := strings.TrimPrefix(r.URL.Path, "/api/v1/task/")
	id = strings.Trim(id, "/")

	switch r.Method {
	case http.MethodGet:
		if id == "" {
			getAllTasks(w, r)
		} else {
			getTaskByID(w, r, id)
		}
	case http.MethodPost:
		createTask(w, r)
	case http.MethodPut:
		updateTask(w, r, id)
	case http.MethodDelete:
		deleteTask(w, r, id)
	default:
		http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed)
	}
}

// getAllTasks возвращает список всех задач.
func getAllTasks(w http.ResponseWriter, r *http.Request) {
	mutex.Lock()
	defer mutex.Unlock()

	var tasks []Task
	for _, task := range tasksMap {
		tasks = append(tasks, task)
	}

	jsonData, err := json.Marshal(tasks)
	if err != nil {
		http.Error(w, "Ошибка при сериализации JSON", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Write(jsonData)
}

// getTaskByID возвращает задачу по её ID.
func getTaskByID(w http.ResponseWriter, r *http.Request, id string) {
	mutex.Lock()
	defer mutex.Unlock()

	task, exists := tasksMap[id]
	if !exists {
		http.Error(w, "Задача не найдена", http.StatusNotFound)
		return
	}

	jsonData, err := json.Marshal(task)
	if err != nil {
		http.Error(w, "Ошибка при сериализации JSON", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Write(jsonData)
}

// createTask создаёт новую задачу.
func createTask(w http.ResponseWriter, r *http.Request) {
	var t Task

	// Декодируем JSON из тела запроса.
	if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
		http.Error(w, "Некорректный JSON", http.StatusBadRequest)
		return
	}

	// Проверяем, что передан непустой ID (для упрощения).
	if t.ID == "" {
		http.Error(w, "Поле 'id' не должно быть пустым", http.StatusBadRequest)
		return
	}

	mutex.Lock()
	defer mutex.Unlock()

	// Проверяем, нет ли уже задачи с таким ID.
	if _, exists := tasksMap[t.ID]; exists {
		http.Error(w, "Задача с таким ID уже существует", http.StatusConflict)
		return
	}

	// Сохраняем задачу в карту.
	tasksMap[t.ID] = t

	// Возвращаем созданный объект.
	jsonData, err := json.Marshal(t)
	if err != nil {
		http.Error(w, "Ошибка при сериализации JSON", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusCreated)
	w.Write(jsonData)
}

// updateTask обновляет задачу с указанным ID.
func updateTask(w http.ResponseWriter, r *http.Request, id string) {
	if id == "" {
		http.Error(w, "Не указан ID задачи", http.StatusBadRequest)
		return
	}

	var t Task
	if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
		http.Error(w, "Некорректный JSON", http.StatusBadRequest)
		return
	}

	mutex.Lock()
	defer mutex.Unlock()

	_, exists := tasksMap[id]
	if !exists {
		http.Error(w, "Задача не найдена", http.StatusNotFound)
		return
	}

	// Обновляем данные задачи (предполагается, что ID не меняется).
	t.ID = id
	tasksMap[id] = t

	jsonData, err := json.Marshal(t)
	if err != nil {
		http.Error(w, "Ошибка при сериализации JSON", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Write(jsonData)
}

// deleteTask удаляет задачу по её ID.
func deleteTask(w http.ResponseWriter, r *http.Request, id string) {
	if id == "" {
		http.Error(w, "Не указан ID задачи", http.StatusBadRequest)
		return
	}

	mutex.Lock()
	defer mutex.Unlock()

	_, exists := tasksMap[id]
	if !exists {
		http.Error(w, "Задача не найдена", http.StatusNotFound)
		return
	}

	delete(tasksMap, id)
	w.WriteHeader(http.StatusNoContent)
}

Итоги будем подводить сразу:

17 баллов + 4 (за выполнение требований) + 1 (работоспособность) + 1 (когнитивная сложность) + 2 (документирование) + 1 (пример для запуска) = 25 баллов.

Доп. требования 5 и 6 не были выполнены, а вот с документированием уже привычно все хорошо. Использованы корректные статусы - 409 при попытке создать задачу с таким же инентификатором, 404 при отсутствии задачи по указанному идентификатору и 403 при некорректном теле запроса или отсутствии идентификатора в path parameter.

А вот реализация от DeepSeek R1:

Реализация CRUD от DeepSeek
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"
)

type Task struct {
	ID        int       `json:"id"`
	Title     string    `json:"title"`
	Completed bool      `json:"completed"`
	CreatedAt time.Time `json:"created_at"`
}

type TaskStore struct {
	sync.RWMutex
	tasks  map[int]Task
	nextID int
}

func (ts *TaskStore) CreateTask(title string) Task {
	ts.Lock()
	defer ts.Unlock()

	task := Task{
		ID:        ts.nextID,
		Title:     title,
		CreatedAt: time.Now(),
	}
	ts.tasks[ts.nextID] = task
	ts.nextID++
	return task
}

func (ts *TaskStore) GetTask(id int) (Task, bool) {
	ts.RLock()
	defer ts.RUnlock()

	task, exists := ts.tasks[id]
	return task, exists
}

func (ts *TaskStore) GetAllTasks() []Task {
	ts.RLock()
	defer ts.RUnlock()

	var tasks []Task
	for _, task := range ts.tasks {
		tasks = append(tasks, task)
	}
	return tasks
}

func (ts *TaskStore) UpdateTask(id int, title string, completed bool) (Task, bool) {
	ts.Lock()
	defer ts.Unlock()

	task, exists := ts.tasks[id]
	if !exists {
		return Task{}, false
	}

	task.Title = title
	task.Completed = completed
	ts.tasks[id] = task
	return task, true
}

func (ts *TaskStore) DeleteTask(id int) bool {
	ts.Lock()
	defer ts.Unlock()

	_, exists := ts.tasks[id]
	if !exists {
		return false
	}

	delete(ts.tasks, id)
	return true
}

type TaskServer struct {
	store *TaskStore
}

func (ts *TaskServer) handleTasks(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET":
		tasks := ts.store.GetAllTasks()
		respondWithJSON(w, http.StatusOK, tasks)
	case "POST":
		var request struct {
			Title string `json:"title"`
		}
		if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
			http.Error(w, "Invalid request body", http.StatusBadRequest)
			return
		}

		task := ts.store.CreateTask(request.Title)
		respondWithJSON(w, http.StatusCreated, task)
	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func (ts *TaskServer) handleSingleTask(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/api/v1/task/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Invalid task ID", http.StatusBadRequest)
		return
	}

	switch r.Method {
	case "GET":
		task, exists := ts.store.GetTask(id)
		if !exists {
			http.NotFound(w, r)
			return
		}
		respondWithJSON(w, http.StatusOK, task)
	case "PUT":
		var request struct {
			Title     string `json:"title"`
			Completed bool   `json:"completed"`
		}
		if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
			http.Error(w, "Invalid request body", http.StatusBadRequest)
			return
		}

		task, updated := ts.store.UpdateTask(id, request.Title, request.Completed)
		if !updated {
			http.NotFound(w, r)
			return
		}
		respondWithJSON(w, http.StatusOK, task)
	case "DELETE":
		deleted := ts.store.DeleteTask(id)
		if !deleted {
			http.NotFound(w, r)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func respondWithJSON(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(data); err != nil {
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
	}
}

func main() {
	store := &TaskStore{
		tasks:  make(map[int]Task),
		nextID: 1,
	}
	server := &TaskServer{store: store}

	mux := http.NewServeMux()
	mux.HandleFunc("/api/v1/task", server.handleTasks)
	mux.HandleFunc("/api/v1/task/", server.handleSingleTask)

	fmt.Println("Server is running on http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}

Сразу бросается в глаза использование RLock/RUnlock для операций чтения, использование структуры с мапой и мьютексом вместо глобальных переменных, как это сделал ChatGPT, дополнительные поля с признаком завершения задачи и датой создания , автоматическая генерация идентификаторов.

Из минорных проблем могу отметить захардкоженные названия HTTP-методов - для этого существуют константы в пакете net/http.

Итого у DeepSeek R1 9 баллов + 5 (за выполнение требований) + 1 (работоспособность) + 1 (пример для запуска) = 16 баллов.

В целом, по функционалу и логике операций реализация от DeepSeek мне понравилась больше, но GPT заработал баллы за документирование и более простой код по сравнению с кодом от DeepSeek.


На этом я бы уже завершил "баттл", но под конец попробуем реализовать что-то более веселое. А именно - алгоритм сортировки StalinSort.

Если в кратце говорить про алгоритм - то StalinSort линейно проходит по всему массиву элементов, сравнивая текущий элемент со следующим. И если следующий элемент меньше, чем текущий - то он просто удаляется из массива либо же не добавляется в результирующий массив (это уже зависит от реализации).

И сразу объявлю небольшой конкурс для читателей. Суть конкурса - угадать, где чья реализация. А наградой победителю пускай будет бумажный вариант книги "Язык программирования Go" от Алана Донована и Брайана Кернигана. Кстати, есть интересная статья с текстовым вариантом интервью с авторами книги.

Сам алгоритм максимально простой, и поэтому стоит обращать внимание на другие детали.

Реализация 1:


// StalinSort принимает срез целых чисел и возвращает "отсортированную" версию,
// удаляя все элементы, нарушающие неубывающий порядок.
func StalinSort(a []int) []int {
    if len(a) == 0 {
        return a
    }

    // Инициализируем результат первым элементом
    result := []int{a[0]}

    // Проходим по оставшимся элементам
    for i := 1; i < len(a); i++ {
        // Проверяем, не нарушает ли текущий элемент неубывающий порядок
        if a[i] >= result[len(result)-1] {
            result = append(result, a[i])
        }
        // Если элемент меньше предыдущего добавленного — "исключаем" его
    }

    return result
}

Реализация 2:

func stalinSort(arr []int) []int {
	if len(arr) == 0 {
		return arr
	}

	// Создаем результирующий срез с первым элементом
	sorted := []int{arr[0]}

	// Проходим по оставшимся элементам
	for i := 1; i < len(arr); i++ {
		// Сохраняем только элементы, которые больше или равны последнему добавленному
		if arr[i] >= sorted[len(sorted)-1] {
			sorted = append(sorted, arr[i])
		}
	}

	return sorted
}

Победителем будет первый человек, который напишет правильный ответ в комментариях.


А теперь пришло время подвести итоги этого "AI-баттла":

С большим отрывом в 9 баллов побеждает ChatGPT o1, который набрал 25 баллов против 16 у DeepSeek R1. Да, эта оценка довольно субъективная, задачи были простые и в иных ситуациях DeepSeek мог бы проявить себя лучше GPT. Но статью прошу исключительно в качестве развлекательного чтения и выбор ИИ-агента в любом случае остается за вами. И для этого в конце оставлю голосовалку.

Мой вывод такой: в плане программирования ChatGPT o1 справляется лучше своего конкурента, будь то корректная реализация всех требований или документирование кода. Но плюсы к карме DeepSeek конечно дают то, что он бесплатный и работает в РФ без использования VPN.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Кто по вашему мнению справился с задачами лучше?
70% ChatGPT o121
30% DeepSeek R19
Проголосовали 30 пользователей. Воздержались 14 пользователей.