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

в 19:16, , рубрики: chatgpt, deepseek, Go, golang

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

Змейка от ChatGPT

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

Пробежимся по коду:
- 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

Визуально обе змейки выглядят почти идентично, но вот у 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. Проведем ревью:

Минусы:

  1. Запросы так же не логируются.

  2. Нет обработки ошибки энкодинга структуры в JSON после создания задачи.

  3. Неявное получение идентификатора задачи из запроса - если бы вместо http.HandleFunc("/task/"...) было http.HandleFunc("/task/{id}"...), то в методах вместо idStr := r.URL.Path[len("/task/"):] можно было бы использовать просто idStr := r.PathValue("id")

Плюсы:

  1. Запросы разделены не по эндпоинтам, а по методам (POST, GET, PUT и DELETE для своих задач, все как мы любим), а использование остальных методов заканчивается возвратом статуса 405.

  2. Большой плюс за автоматическое назначение идентификаторов задачам.

  3. Использованы мьютексы, разблокировка в defer.

  4. В функции updateTask учтена ситуация, когда в path parameter запроса передаем один идентификатор, а в теле - другой (ID в теле запроса заменяется на тот, что отправили в path parameter).

Думаю все согласятся, что очередное очко заслуживает именно DeepSeek, который и выходит победителем из этого "AI-баттла".

Разумеется, моя оценка субъективна и у каждого есть свое мнение на этот счет, которое вы можете выразить в опросе ниже. Данная статья носит больше развлекательный характер с наблюдением за битвой двух генеративных моделей и не является ИНР (индивидуальной нейронной рекомендацией).

Автор: starwalkn

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js