Сравнение кода от DeepSeek и ChatGPT по-гоферски
- понедельник, 3 февраля 2025 г. в 00:00:14
В поисках утреннего чтива на Хабре наткнулся на статью про сравнение кода на Swift, сгенерированного двумя действующими лицами - ChatGPT от OpenAI и его нашумевший "убийца" - DeepSeek, основателем которого выступает Лян Вэньфэн, про которого можно узнать в статье на VC.
После прочтения статьи в голову вполне ожидаемо пришла идея сравнения сгенерированного кода на Go. Правила те же самые - использование одинаковых промптов для каждой модели. Если модель выдает неработоспособный результат - даем еще одну попытку, но условно ставим "незачет".
В статье автор сравнивал устаревшие версии GPT 4o и DeepSeek V3. Здесь же будет сравнение более актуальных моделей - GPT o1 и DeepSeek R1 (DeepThink).
Сравнивать результаты буду по следующим критериям:
Работоспособность - 1 балл. Присваиваем, если код успешно компилируется и запускается.
Выполнение требований по задаче - по 1 баллу за каждое требование.
Когнитивная сложность - 1 балл. Моя субъективная оценка сложности восприятия кода, достается только одному.
Документирование кода - 2 балла. 1 балл за комментарии к функциям, 1 балл за комментарии к определенным кускам кода.
Пример для запуска - 1 балл. Условно это должна быть функция main, которая содержит готовые данные для проверки работоспособности кода (кроме змейки, она и так должна быть исполняемой программой).
Дизайн змейки - 1 балл. Моя субъективная оценка.
Реализация сложности змейки - 1 балл. Моя субъективная оценка. Отличается от сложности в требованиях тем, что балл дается за качество реализации сложности.
Система баллов оставляет желать лучшего, но у меня не было желания для более тщательной проработки.
Начнем мы так же, как и во вдохновившей меня статье - всеми любимой "змейки". Для генерации был использован такой промпт:
Напиши реализацию простого варианта игры "змейка" на языке Go для запуска в терминале. Змейка должна быть зеленого цвета, еда - красного. Поле должно иметь границы 15 на 15. Должен отображаться счетчик еды. Перед началом игры должна быть возможность выбрать сложность игры, от которого будет зависеть скорость змейки - низкая, средняя и высокая. Управление змейкой должно осуществляться стрелками на клавиатуре. Из внешних зависимостей может быть использована только библиотека https://github.com/charmbracelet/bubbletea.
Соберем требования в список:
Змейка зеленого цвета, еда красного.
Границы 15 на 15 символов.
Счетчик съедненной еды.
Реализация разных версий сложности игры.
Управление стрелками.
Вот что нам сгенерировал ChatGPT o1:
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 получает за змейку 4 (по требованиям) + 1 (работоспособность) + 1 (когнитивная сложность) + 2 (документирование) + 1 (реализация сложности) = 9 баллов. За дизайн не стал добавлять балл из-за отсутствия визуализации границ.
А вот что получилось у DeepSeek R1:
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.
Снова собираем требования в список для удобства оценки:
Колчичество воркеров и размер буфера каналов задается пользователем.
Задачи разделены на два типа приоритетности - HighPriority и LowPriority.
Перед завершением программы нужно гарантировать выполнение всех задач с высоким приоритетом.
Реализация от ChatGPT o1:
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 9 баллов + 3 (за выполнение требований) + 1 (работоспособность) + 1 (когнитивная сложность) + 2 (документирование) + 1 (пример для запуска) = 17 баллов.
А теперь посмотрим на ответ 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 R1: сейчас у него 6 баллов + 2 (за выполнение требований) + 1 (работоспособность) = 9 баллов. Снова нет баллов за документирование (комментариев нет вообще), отсутствовал пример для запуска и балл за когнитивную сложность решил отдать GPT o1.
Последней задачей будет реализация простого веб-сервера для CRUD-операций над задачами.
Промт составил следующий:
Напиши простой веб-сервер на Go, который позволяет выполнять CRUD-операции над сущностью "Задача (Task)". Используй только стандартную библиотеку Go, сторонние пакеты запрещены. Эндпоинты должны иметь вид `api/v1/task`. В качестве хранилища используй стандартный тип map.
Да, здесь описано мало требований, таких как разделение эндпоинтов по методам (POST для создания, PUT для обновления и так далее), но это сделано специально, чтобы в результатах было больше "отсебятины".
А наши требование таковы:
Обязательная реализация 4-х методов - для получения, создания, обновления и удаления задач.
Хранилище (в нашем случае map) должно быть потокобезопасным.
Дополнительный балл за разделение операций "по REST" - то есть использование одного эндпоинта с разными HTTP-методами.
Дополнительный балл за использование корректных HTTP-статусов в зависимости от природы ошибки.
Дополнительный балл за логирование запросов в любом виде (не было в требованиях промпта).
Дополнительный балл за автоматическую генерацию идентификаторов задач (не было в требованиях промпта).
Посмотрим на реализацию GPT o1:
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:
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.