В поисках утреннего чтива на Хабре наткнулся на статью про сравнение кода на 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:
Код от 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 получает за змейку 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.
Снова собираем требования в список для удобства оценки:
-
Колчичество воркеров и размер буфера каналов задается пользователем.
-
Задачи разделены на два типа приоритетности — HighPriority и LowPriority.
-
Перед завершением программы нужно гарантировать выполнение всех задач с высоким приоритетом.
Реализация от 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 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 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:
Реализация 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.
ссылка на оригинал статьи https://habr.com/ru/articles/878758/
Добавить комментарий