В поисках утреннего чтива на Хабре наткнулся на статью про сравнение кода на Swift, сгенерированного двумя действующими лицами - ChatGPT от OpenAI и его нашумевший "убийца" - DeepSeek, основателем которого выступает Лян Вэньфэн, про которого можно узнать в статье на VC.
После прочтения статьи в голову вполне ожидаемо пришла идея сравнения сгенерированного кода на Go. Правила те же самые - одинаковые промпты, если с результатом все совсем плохо - даем еще одну попытку, но условно ставим "незачет".
На ринге - GPT-4o и DeepSeek-V3 (во время испытаний ни одна модель не пострадала).
А начнем мы так же, как и в статье из шапки - всеми любимой "змейки" (хороший тамада, и конкурсы интересные). Для теста был использован такой промпт:
Напиши с нуля простейший вариант игры змейка на Go для запуска в терминале. Цвет змеи сделай зеленым. Управление с помощью стрелок на клавиатуре.
Вот что нам сгенерировал ChatGPT:
Код от ChatGPT
package main
import (
"fmt"
"math/rand"
"time"
"github.com/nsf/termbox-go"
)
type Point struct {
x, y int
}
var (
width, height = 20, 10
snake = []Point{{5, 5}}
direction = Point{1, 0}
food = Point{10, 5}
gameOver = false
)
func main() {
rand.Seed(time.Now().UnixNano())
_ = termbox.Init()
defer termbox.Close()
go inputHandler()
for !gameOver {
render()
time.Sleep(150 * time.Millisecond)
update()
}
fmt.Println("Game Over!")
}
func inputHandler() {
for {
switch ev := termbox.PollEvent(); ev.Type {
case termbox.EventKey:
switch ev.Key {
case termbox.KeyArrowUp:
if direction.y == 0 {
direction = Point{0, -1}
}
case termbox.KeyArrowDown:
if direction.y == 0 {
direction = Point{0, 1}
}
case termbox.KeyArrowLeft:
if direction.x == 0 {
direction = Point{-1, 0}
}
case termbox.KeyArrowRight:
if direction.x == 0 {
direction = Point{1, 0}
}
case termbox.KeyEsc, termbox.KeyCtrlC:
gameOver = true
}
}
}
}
func update() {
head := Point{snake[0].x + direction.x, snake[0].y + direction.y}
if head.x < 0 || head.x >= width || head.y < 0 || head.y >= height {
gameOver = true
return
}
for _, p := range snake {
if p == head {
gameOver = true
return
}
}
snake = append([]Point{head}, snake...)
if head == food {
placeFood()
} else {
snake = snake[:len(snake)-1]
}
}
func placeFood() {
for {
food = Point{rand.Intn(width), rand.Intn(height)}
valid := true
for _, p := range snake {
if p == food {
valid = false
break
}
}
if valid {
break
}
}
}
func render() {
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
for _, p := range snake {
termbox.SetCell(p.x, p.y, 'O', termbox.ColorGreen, termbox.ColorDefault)
}
termbox.SetCell(food.x, food.y, '@', termbox.ColorRed, termbox.ColorDefault)
termbox.Flush()
}

Сразу бросается в глаза отсутствие видимых границ (но они есть). Это можно оправдать уровнем "хардкор", но геймер из меня так себе.
Пробежимся по коду:
- ChatGPT использовал дополнительную зависимость "github.com/nsf/termbox-go".
- Между рендерингом и обновлением полагается на time.Sleep(150 * time.Millisecond), то есть ждем 150 миллисекунд после рендера.
А вот что получилось у DeepSeek:
Код от DeepSeek
package main
import (
"fmt"
"log"
"math/rand"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"github.com/eiannone/keyboard"
)
const (
width = 20
height = 10
)
type point struct {
x, y int
}
var (
snake []point
food point
direction = "RIGHT"
gameOver = false
score = 0
)
func clearScreen() {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
cmd.Run()
}
func draw() {
clearScreen()
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if x == 0 || x == width-1 || y == 0 || y == height-1 {
fmt.Print("#")
} else if contains(snake, point{x, y}) {
fmt.Print("33[32mO33[0m") // Зеленый цвет для змейки
} else if x == food.x && y == food.y {
fmt.Print("33[31m*33[0m") // Красный цвет для еды
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
fmt.Println("Score:", score)
}
func contains(s []point, p point) bool {
for _, v := range s {
if v == p {
return true
}
}
return false
}
func update() {
head := snake[0]
var newHead point
switch direction {
case "UP":
newHead = point{head.x, head.y - 1}
case "DOWN":
newHead = point{head.x, head.y + 1}
case "LEFT":
newHead = point{head.x - 1, head.y}
case "RIGHT":
newHead = point{head.x + 1, head.y}
}
if newHead.x <= 0 || newHead.x >= width-1 || newHead.y <= 0 || newHead.y >= height-1 || contains(snake, newHead) {
gameOver = true
return
}
snake = append([]point{newHead}, snake...)
if newHead == food {
score++
placeFood()
} else {
snake = snake[:len(snake)-1]
}
}
func placeFood() {
food = point{rand.Intn(width-2) + 1, rand.Intn(height-2) + 1}
if contains(snake, food) {
placeFood()
}
}
func setup() {
rand.Seed(time.Now().UnixNano())
snake = []point{{width / 2, height / 2}}
placeFood()
}
func main() {
setup()
if err := keyboard.Open(); err != nil {
log.Fatal(err)
}
defer keyboard.Close()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
gameOver = true
}()
go func() {
for !gameOver {
char, key, err := keyboard.GetKey()
if err != nil {
gameOver = true
return
}
switch {
case key == keyboard.KeyArrowUp && direction != "DOWN":
direction = "UP"
case key == keyboard.KeyArrowDown && direction != "UP":
direction = "DOWN"
case key == keyboard.KeyArrowLeft && direction != "RIGHT":
direction = "LEFT"
case key == keyboard.KeyArrowRight && direction != "LEFT":
direction = "RIGHT"
case char == 'q' || char == 'Q':
gameOver = true
}
}
}()
for !gameOver {
draw()
update()
time.Sleep(100 * time.Millisecond)
}
clearScreen()
fmt.Println("Game Over! Final Score:", score)
}

Визуально обе змейки выглядят почти идентично, но вот у DeepSeek получилось отрисовать границы, что упрощает нам игру, но при этом он добавил змейке скорости. Я бы назвал это уровнем "Средний".
Что по коду?
- DeepSeek также использовал стороннюю зависимость "github.com/eiannone/keyboard" для идентификации нажатий клавиатуры.
- Изменить цвета проще в коде от ChatGPT, DeepSeek же решил использовать ANSI escape-коды.
- Так же полагается на time.Sleep(100 * time.Millisecond).
С учетом того что ChatGPT не стал отрисовывать границы полей, 1 очко заслуженно достается DeepSeek.
Следующим испытанием будет реализация веб-сервера с примитивными CRUD-операциями над задачами (tasks). Промпт следующий:
Напиши простейший веб-сервер на Go, у которого будут 4 эндпоинта для реализации CRUD-операций с сущностью "Задача (Task)". В качестве базы данных используй стандартный тип map.
Результат от ChatGPT:
CRUD от ChatGPT
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
)
type Task struct {
ID string `json:"id"`
Name string `json:"name"`
}
var (
tasks = make(map[string]Task)
mutex = sync.Mutex{}
)
func createTask(w http.ResponseWriter, r *http.Request) {
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
mutex.Lock()
tasks[task.ID] = task
mutex.Unlock()
w.WriteHeader(http.StatusCreated)
}
func getTask(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
mutex.Lock()
task, exists := tasks[id]
mutex.Unlock()
if !exists {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(task)
}
func updateTask(w http.ResponseWriter, r *http.Request) {
var updatedTask Task
if err := json.NewDecoder(r.Body).Decode(&updatedTask); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}
mutex.Lock()
if _, exists := tasks[updatedTask.ID]; !exists {
mutex.Unlock()
http.Error(w, "Task not found", http.StatusNotFound)
return
}
tasks[updatedTask.ID] = updatedTask
mutex.Unlock()
w.WriteHeader(http.StatusOK)
}
func deleteTask(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
mutex.Lock()
if _, exists := tasks[id]; !exists {
mutex.Unlock()
http.Error(w, "Task not found", http.StatusNotFound)
return
}
delete(tasks, id)
mutex.Unlock()
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/task/create", createTask)
http.HandleFunc("/task/get", getTask)
http.HandleFunc("/task/update", updateTask)
http.HandleFunc("/task/delete", deleteTask)
log.Println("Server started on :8085")
log.Fatal(http.ListenAndServe(":8085", nil))
}
И сразу проведем ревью кода, отметив плюсы и минусы:
Минусы:
-
Нет ограничений HTTP-методов для эндпоинтов - для /task/get мы спокойно можем использовать POST-метод и результат будет таким же, как было бы при GET.
-
Разделение операций по эндпоинтам, а не методам, как принято для REST API.
-
Нет никакого логирования входящих запросов.
-
Идентификаторы задач надо отправлять на стороне клиента.
Плюсы:
-
Использован мьютекс для глобального хранилища-мапы.
-
Код максимально простой и примитивный, и при этом работает корректно (а это самое важное).
А теперь посмотрим на ответ DeepSeek:
CRUD от DeepSeek
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
}
var (
tasks = make(map[int]Task)
nextID = 1
mu sync.Mutex
)
func main() {
http.HandleFunc("/tasks", getTasks)
http.HandleFunc("/task", createTask)
http.HandleFunc("/task/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getTask(w, r)
case http.MethodPut:
updateTask(w, r)
case http.MethodDelete:
deleteTask(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
fmt.Println("Server is running on port 8080...")
http.ListenAndServe(":8080", nil)
}
func getTasks(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
taskList := make([]Task, 0, len(tasks))
for _, task := range tasks {
taskList = append(taskList, task)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(taskList)
}
func createTask(w http.ResponseWriter, r *http.Request) {
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
task.ID = nextID
nextID++
tasks[task.ID] = task
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(task)
}
func getTask(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/task/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid task ID", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
task, exists := tasks[id]
if !exists {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(task)
}
func updateTask(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/task/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid task ID", http.StatusBadRequest)
return
}
var updatedTask Task
if err := json.NewDecoder(r.Body).Decode(&updatedTask); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
if _, exists := tasks[id]; !exists {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
updatedTask.ID = id
tasks[id] = updatedTask
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(updatedTask)
}
func deleteTask(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Path[len("/task/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid task ID", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
if _, exists := tasks[id]; !exists {
http.Error(w, "Task not found", http.StatusNotFound)
return
}
delete(tasks, id)
w.WriteHeader(http.StatusNoContent)
}
На первый взгляд код выглядит чуточку интереснее по сравнению с ответом от ChatGPT. Проведем ревью:
Минусы:
-
Запросы так же не логируются.
-
Нет обработки ошибки энкодинга структуры в JSON после создания задачи.
-
Неявное получение идентификатора задачи из запроса - если бы вместо
http.HandleFunc("/task/"...)
былоhttp.HandleFunc("/task/{id}"...)
, то в методах вместоidStr := r.URL.Path[len("/task/"):]
можно было бы использовать простоidStr := r.PathValue("id")
Плюсы:
-
Запросы разделены не по эндпоинтам, а по методам (POST, GET, PUT и DELETE для своих задач, все как мы любим), а использование остальных методов заканчивается возвратом статуса 405.
-
Большой плюс за автоматическое назначение идентификаторов задачам.
-
Использованы мьютексы, разблокировка в
defer
. -
В функции
updateTask
учтена ситуация, когда в path parameter запроса передаем один идентификатор, а в теле - другой (ID в теле запроса заменяется на тот, что отправили в path parameter).
Думаю все согласятся, что очередное очко заслуживает именно DeepSeek, который и выходит победителем из этого "AI-баттла".
Разумеется, моя оценка субъективна и у каждого есть свое мнение на этот счет, которое вы можете выразить в опросе ниже. Данная статья носит больше развлекательный характер с наблюдением за битвой двух генеративных моделей и не является ИНР (индивидуальной нейронной рекомендацией).
Автор: starwalkn