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

от автора

В поисках утреннего чтива на Хабре наткнулся на статью про сравнение кода на 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.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Кто по вашему мнению справился с задачами лучше?

73.53% ChatGPT o150
26.47% DeepSeek R118

Проголосовали 68 пользователей. Воздержались 35 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/878758/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *