Использование классов

Узнайте, как использовать обертки классов для более чистых API плагинов, основанных на объектно-ориентированном программировании, в Go.

Классы в Plugify предоставляют объектно-ориентированный способ работы с ресурсами плагинов, обеспечивая управление жизненным циклом и более чистые API. Вместо ручного управления дескрипторами и вызова функций с явными указателями, вы можете использовать интуитивные интерфейсы на основе структур с идиоматичной обработкой ошибок Go.

Зачем использовать классы?

Работа с API на основе дескрипторов в традиционном стиле C может быть многословной и подверженной ошибкам:

Без классов (стиль C)
package main

import "test_keyvalues"

func processConfig() {
    // Создать дескриптор KeyValues вручную
    kvHandle := test_keyvalues.Kv1Create("MyConfig")

    // Установить свойства, используя дескриптор
    test_keyvalues.Kv1SetName(kvHandle, "ServerSettings")

    // Найти подключ
    subkeyHandle := test_keyvalues.Kv1FindKey(kvHandle, "Players")

    // Легко забыть очистку!
    test_keyvalues.Kv1Destroy(subkeyHandle)
    test_keyvalues.Kv1Destroy(kvHandle)
}

С классами тот же код становится намного чище и безопаснее:

С классами (стиль ООП)
package main

import "test_keyvalues"

func processConfig() {
    // Создать с использованием конструктора
    kv := test_keyvalues.NewKeyValuesKv1Create("MyConfig")
    defer kv.Close()  // Автоматическая очистка при возврате из функции

    // Использовать интуитивные методы с обработкой ошибок
    if err := kv.SetName("ServerSettings"); err != nil {
        panic(err)
    }

    // Найти подключ - возвращает экземпляр KeyValues
    subkey, err := kv.FindKey("Players")
    if err != nil {
        panic(err)
    }

    // Автоматическая очистка через defer!
}

Преимущества:

  • Более чистый синтаксис - Методы вместо функций с явными дескрипторами
  • Идиоматичная обработка ошибок Go - Методы возвращают error вместо использования исключений
  • Очистка на основе defer - Используйте defer для детерминированной очистки ресурсов на основе области видимости
  • Безопасность типов - Лучшее автодополнение в IDE и проверки во время компиляции
  • Меньше ошибок - Труднее забыть очистку или перепутать дескрипторы
  • Предотвращение копирования - Защита noCopy гарантирует, что дескрипторы не будут случайно скопированы
  • Отслеживание владения - Явная семантика владения предотвращает ошибки двойного освобождения

Важный паттерн:

Как работают классы

Когда плагин определяет классы в своем манифесте, Plugify автоматически генерирует обертки структур Go, которые:

  1. Оборачивают базовый дескриптор - Хранят необработанный указатель внутри как uintptr
  2. Связывают методы - Преобразуют вызовы функций в вызовы методов с автоматической передачей дескриптора
  3. Управляют жизненным циклом - Регистрируются в runtime.Cleanup для недетерминированной финализации
  4. Предоставляют утилитарные методы - Get(), Release(), Reset(), IsValid(), Close() для управления дескриптором
  5. Отслеживают владение - Используют тип ownership для предотвращения ошибок двойного освобождения
  6. Предотвращают копирование - Включают защиту noCopy для обнаружения случайных копий
  7. Валидируют дескрипторы - Автоматически проверяют действительность дескриптора перед вызовами методов
  8. Возвращают ошибки - Используют идиоматичную обработку ошибок Go для всех операций

Определение классов в вашем манифесте

Чтобы создать классы для вашего плагина, добавьте секцию classes в ваш манифест:

plugin.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "language": "cpp",
  "methods": [
    {
      "name": "Kv1Create",
      "funcName": "Kv1Create",
      "paramTypes": [
        { "name": "setName", "type": "string" }
      ],
      "retType": { "type": "ptr64" }
    },
    {
      "name": "Kv1Destroy",
      "funcName": "Kv1Destroy",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "void" }
    },
    {
      "name": "Kv1GetName",
      "funcName": "Kv1GetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" }
      ],
      "retType": { "type": "string" }
    },
    {
      "name": "Kv1SetName",
      "funcName": "Kv1SetName",
      "paramTypes": [
        { "name": "kv", "type": "ptr64" },
        { "name": "name", "type": "string" }
      ],
      "retType": { "type": "void" }
    }
  ],
  "classes": [
    {
      "name": "KeyValues",
      "description": "RAII wrapper for KeyValues handle",
      "handleType": "ptr64",
      "invalidValue": "0",
      "constructors": ["Kv1Create"],
      "destructor": "Kv1Destroy",
      "bindings": [
        {
          "name": "GetName",
          "method": "Kv1GetName",
          "bindSelf": true
        },
        {
          "name": "SetName",
          "method": "Kv1SetName",
          "bindSelf": true
        }
      ]
    }
  ]
}

Объяснение ключевых полей:

  • name: Имя структуры Go (требуется PascalCase)
  • handleType: Тип базового дескриптора (обычно ptr64 или ptr32)
  • invalidValue: Какое значение представляет недопустимый дескриптор (обычно "0" или "-1")
  • constructors: Массив имен методов, которые создают экземпляры
  • destructor: Имя метода, который очищает ресурсы (опционально)
  • bindings: Массив методов, доступных в структуре
    • bindSelf: Если true, автоматически передает дескриптор в качестве первого параметра

Сгенерированный код Go

Когда вы определяете классы в своем манифесте, Plugify генерирует обертки структур Go с несколькими встроенными функциями:

Сгенерированная структура (концептуально)
package test_keyvalues

import (
    "errors"
    "runtime"
)

// noCopy предотвращает копирование через go vet
type noCopy struct{}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

// ownership указывает, владеет ли экземпляр базовым дескриптором
type ownership bool

const (
    Owned    ownership = true
    Borrowed ownership = false
)

var (
    KeyValuesErrEmptyHandle = errors.New("KeyValues: empty handle")
)

// KeyValues - RAII обертка для дескриптора KeyValues
type KeyValues struct {
    handle    uintptr
    cleanup   runtime.Cleanup
    ownership ownership
    noCopy    noCopy
}

// NewKeyValuesKv1Create создает новый экземпляр KeyValues
func NewKeyValuesKv1Create(setName string) *KeyValues {
    return newKeyValuesOwned(Kv1Create(setName))
}

// newKeyValuesBorrowed создает KeyValues из заимствованного дескриптора (внутреннее использование)
func newKeyValuesBorrowed(handle uintptr) *KeyValues {
    if handle == 0 {
        return &KeyValues{}
    }
    return &KeyValues{
        handle:    handle,
        ownership: Borrowed,
    }
}

// newKeyValuesOwned создает KeyValues из принадлежащего дескриптора (внутреннее использование)
func newKeyValuesOwned(handle uintptr) *KeyValues {
    if handle == 0 {
        return &KeyValues{}
    }
    w := &KeyValues{
        handle:    handle,
        ownership: Owned,
    }
    w.cleanup = runtime.AddCleanup(w, w.finalize, struct{}{})
    return w
}

// finalize - функция финализатора (как деструктор C++)
func (w *KeyValues) finalize(_ struct{}) {
    if plugify.Plugin.Loaded {
        w.destroy()
    }
}

// destroy очищает принадлежащие дескрипторы
func (w *KeyValues) destroy() {
    if w.handle != 0 && w.ownership == Owned {
        Kv1Destroy(w.handle)
    }
}

// nullify сбрасывает дескриптор
func (w *KeyValues) nullify() {
    w.handle = 0
    w.ownership = Borrowed
}

// Close явно уничтожает дескриптор (как деструктор C++, но вручную)
func (w *KeyValues) Close() {
    w.Reset()
}

// Get возвращает базовый дескриптор
func (w *KeyValues) Get() uintptr {
    return w.handle
}

// Release освобождает владение и возвращает дескриптор
func (w *KeyValues) Release() uintptr {
    if w.ownership == Owned {
        w.cleanup.Stop()
    }
    handle := w.handle
    w.nullify()
    return handle
}

// Reset уничтожает и сбрасывает дескриптор
func (w *KeyValues) Reset() {
    if w.ownership == Owned {
        w.cleanup.Stop()
    }
    w.destroy()
    w.nullify()
}

// IsValid возвращает true, если дескриптор не nil
func (w *KeyValues) IsValid() bool {
    return w.handle != 0
}

// Связанные методы (с автоматической валидацией дескриптора)
// GetName получает имя секции экземпляра KeyValues
func (w *KeyValues) GetName() (string, error) {
    if w.handle == 0 {
        var zero string
        return zero, KeyValuesErrEmptyHandle
    }
    return Kv1GetName(w.handle), nil
}

// SetName устанавливает имя секции экземпляра KeyValues
func (w *KeyValues) SetName(name string) error {
    if w.handle == 0 {
        return KeyValuesErrEmptyHandle
    }
    Kv1SetName(w.handle, name)
    return nil
}

// FindKey находит ключ по имени
func (w *KeyValues) FindKey(keyName string) (*KeyValues, error) {
    if w.handle == 0 {
        var zero *KeyValues
        return zero, KeyValuesErrEmptyHandle
    }
    return newKeyValuesBorrowed(Kv1FindKey(w.handle, keyName)), nil
}

// AddSubKey добавляет подключ к этому экземпляру KeyValues
func (w *KeyValues) AddSubKey(subKey *KeyValues) error {
    if w.handle == 0 {
        return KeyValuesErrEmptyHandle
    }
    Kv1AddSubKey(w.handle, subKey.Release())
    return nil
}

Встроенные утилитарные методы

Каждая сгенерированная структура включает несколько утилитарных методов для управления дескриптором:

IsValid() - Проверка действительности дескриптора

Возвращает true, если дескриптор действителен (не равен invalidValue):

Использование IsValid()
package main

import (
    "fmt"
    "test_keyvalues"
)

func checkHandle() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    defer kv.Close()

    if kv.IsValid() {
        kv.SetName("ServerConfig")
        fmt.Println("Handle is valid")
    } else {
        fmt.Println("Handle is invalid")
    }

    // После release дескриптор становится недействительным
    handle := kv.Release()
    fmt.Println(kv.IsValid())  // false

    // Должны вручную очистить освобожденный дескриптор
    test_keyvalues.Kv1Destroy(handle)
}

Get() - Доступ к необработанному дескриптору

Возвращает значение базового дескриптора. Используйте это, когда вам нужно передать необработанный дескриптор в функции в стиле C:

Использование Get()
package main

import (
    "fmt"
    "test_keyvalues"
)

func getRawHandle() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    defer kv.Close()

    // Получить необработанный дескриптор
    rawHandle := kv.Get()
    fmt.Printf("Handle value: %d\n", rawHandle)

    // Передать в функцию, ожидающую необработанный дескриптор
    someCFunction(kv.Get())
}

Release() - Передача владения

Освобождает владение дескриптором и возвращает его. После вызова Release() экземпляр структуры становится недействительным и не будет вызывать деструктор:

Использование Release()
package main

import (
    "fmt"
    "test_keyvalues"
)

func createAndRelease() uintptr {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.SetName("ServerConfig")

    // Передать владение наружу
    handle := kv.Release()

    // kv теперь недействителен, не будет очищать
    fmt.Println(kv.IsValid())  // false

    // Defer не нужен - мы освободили владение
    return handle
}

func useReleased() {
    // Теперь мы владеем дескриптором и должны очистить его вручную
    rawHandle := createAndRelease()
    // ... использовать rawHandle ...
    test_keyvalues.Kv1Destroy(rawHandle)  // Требуется ручная очистка!
}

Используйте Release(), когда вам нужно:

  • Передать владение другой системе
  • Сохранить дескриптор в долгоживущей структуре данных
  • Взаимодействовать с кодом в стиле C, который принимает владение

Close() - Ручная очистка

Явно закрывает дескриптор и вызывает деструктор, если объект владеет им. Это то, что вызывает defer:

Использование Close()
package main

import (
    "fmt"
    "test_keyvalues"
)

func manualClose() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.SetName("ServerConfig")

    // Явно закрыть дескриптор сейчас
    kv.Close()

    // Дескриптор теперь недействителен
    fmt.Println(kv.IsValid())  // false

    // Методы вернут ошибку
    if err := kv.SetName("Test"); err != nil {
        fmt.Printf("Error: %v\n", err)  // "KeyValues: empty handle"
    }
}

Метод Close():

  • Идемпотентный - Безопасно вызывать несколько раз
  • Останавливает финализатор - Предотвращает двойную очистку вызовом cleanup.Stop()
  • Учитывает владение - Вызывает деструктор только если объект владеет дескриптором
  • Дружественен к defer - Разработан для использования с defer

Reset() - Уничтожение и сброс

Уничтожает дескриптор и сбрасывает его в недействительное состояние. Аналогично Close(), но вызывается Close():

Использование Reset()
package main

import (
    "fmt"
    "test_keyvalues"
)

func resetHandle() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.SetName("ServerConfig")

    // Reset уничтожает и сбрасывает
    kv.Reset()

    fmt.Println(kv.IsValid())  // false
}

Управление ресурсами

Классы Go, сгенерированные Plugify, поддерживают детерминированную очистку через defer:

Очистка на основе Defer (рекомендуется)

Оператор defer в Go обеспечивает детерминированную очистку в области видимости функции:

Паттерн Defer (рекомендуется)
package main

import "test_keyvalues"

func processConfig() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    defer kv.Close()  // Гарантированная очистка при возврате из функции

    kv.SetName("ServerConfig")
    kv.SetString("hostname", "My Server")
    // ... использовать kv ...

    // Автоматически очищается здесь через defer
}

Это рекомендуемый паттерн, потому что:

  • Гарантированная очистка даже при панике
  • Четкая область жизни ресурса
  • Детерминированное время очистки
  • Идиоматично для Go

Финализаторы (недетерминированный резерв)

Plugify использует runtime.Cleanup для регистрации финализаторов, которые очищают ресурсы во время сборки мусора. Однако это недетерминировано и на это не следует полагаться:

Резервная финализация
package main

import "test_keyvalues"

func processConfig() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.SetName("ServerConfig")
    // ... использовать kv ...
    // НЕТ defer - полагается на финализацию (ПЛОХО!)
}

// kv в конечном итоге будет очищен финализатором
// но время непредсказуемо - НЕ ПОЛАГАЙТЕСЬ НА ЭТО!

Случай без деструктора

Если в манифесте класс не имеет определенного деструктора, он действует как простая обертка без автоматической очистки:

Обертка без деструктора
package main

import "test_keyvalues"

func useWrapper() {
    // Структура без деструктора - просто удобная обертка
    wrapper := test_keyvalues.NewSomeWrapper()

    // Все еще имеет утилитарные методы
    if wrapper.IsValid() {
        handle := wrapper.Get()
        // ... использовать handle ...
    }

    // Нет автоматической очистки - дескриптор сохраняется
    // Полезно для оберток без состояния или глобальных ресурсов
}

Работа с владением

Некоторые методы передают владение ресурсами. Манифест указывает это с помощью поля owner в paramAliases и retAlias.

Принятие владения (параметры метода)

Когда метод принимает владение ресурсом, он автоматически вызывает Release() и вы не должны использовать объект после этого:

Передача владения
package main

import "test_keyvalues"

func transferOwnership() {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
    defer parent.Close()

    child := test_keyvalues.NewKeyValuesKv1Create("Child")
    // НЕ defer child.Close() - parent примет владение

    // AddSubKey принимает владение child
    parent.AddSubKey(child)

    // child теперь принадлежит parent
    // child.IsValid() теперь false (Release() был вызван внутренне)
    // parent обработает очистку
}

В манифесте это определяется как:

Владение в манифесте
{
  "name": "AddSubKey",
  "method": "Kv1AddSubKey",
  "bindSelf": true,
  "paramAliases": [
    {
      "name": "subKey",
      "owner": true  // Этот параметр принимает владение
    }
  ]
}

Лучшая практика: После передачи владения объект автоматически освобождается:

Правильная обработка владения
package main

import "test_keyvalues"

func properOwnership() {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
    defer parent.Close()

    child := test_keyvalues.NewKeyValuesKv1Create("Child")

    // Передать владение - child.Release() вызывается внутренне
    parent.AddSubKey(child)

    // child.IsValid() теперь false
    // Использовать только через parent
    found, err := parent.FindKey("Child")
    if err != nil {
        panic(err)
    }
    _ = found
}

Возврат владения (возвращаемые значения)

Когда метод возвращает новый ресурс с владением:

Возврат владения
package main

import "test_keyvalues"

func returnOwnership() {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
    defer parent.Close()

    // FindKey возвращает НОВЫЙ KeyValues, которым мы владеем
    child, err := parent.FindKey("Settings")
    if err != nil {
        panic(err)
    }
    defer child.Close()  // Мы владеем им, должны очистить

    if child.IsValid() {
        child.SetName("UpdatedSettings")
        // Мы ответственны за жизненный цикл child
    }
}

В манифесте:

Возврат владения в манифесте
{
  "name": "FindKey",
  "method": "Kv1FindKey",
  "bindSelf": true,
  "retAlias": {
    "name": "KeyValues",
    "owner": true  // Вызывающий владеет возвращаемым объектом
  }
}

Невладеющие ссылки

Когда owner: false, метод возвращает ссылку без передачи владения:

Невладеющая ссылка
package main

import "test_keyvalues"

func nonOwningRef() {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
    defer parent.Close()

    // GetFirstSubKey возвращает ссылку, parent все еще владеет ею
    childRef, err := parent.GetFirstSubKey()
    if err != nil {
        panic(err)
    }

    if childRef.IsValid() {
        // Используйте ссылку, но НЕ закрывайте или освобождайте ее
        name, _ := childRef.GetName()
        _ = name
        // childRef будет очищен parent
    }
}

Жизненный цикл плагина и глобальные переменные

При использовании классов в плагинах вы должны быть осторожны с очисткой объектов во время выгрузки плагина. Если объекты все еще живы, когда ваш плагин выгружается, их финализаторы могут быть вызваны после того, как код плагина больше не находится в памяти, что приводит к неопределенному поведению или сбоям.

Управление глобальными переменными

Если вы храните экземпляры классов в глобальных переменных или переменных уровня пакета, вы должны явно очистить их в функции PluginEnd() вашего плагина:

plugin.go
package main

import (
    "github.com/untrustedmodders/go-plugify"
    "test_keyvalues"
)

// Глобальный объект - опасен, если не очищен!
var gConfig *test_keyvalues.KeyValues

func PluginStart() {
    // Создать глобальную конфигурацию
    gConfig = test_keyvalues.NewKeyValuesKv1Create("GlobalConfig")
    gConfig.SetName("ServerSettings")
    plugify.Log("Plugin started with global config")
}

func PluginEnd() {
    // КРИТИЧНО: Очистить глобальные объекты перед выгрузкой плагина
    if gConfig != nil {
        gConfig.Close()
        gConfig = nil
    }
    plugify.Log("Plugin ended, global config cleaned up")
}

Безопасные паттерны

✅ Безопасно: Локальная область видимости с defer

Безопасный паттерн
func onCommand(args []string) {
    // Локальный объект с defer - автоматически очищается
    kv := test_keyvalues.NewKeyValuesKv1Create("TempConfig")
    defer kv.Close()

    kv.SetName("CommandConfig")
    // ... использовать kv ...
    // Автоматически уничтожается при возврате из функции - безопасно!
}

✅ Безопасно: Локальные переменные функции

Безопасный паттерн
func processData() {
    kv := test_keyvalues.NewKeyValuesKv1Create("TempConfig")
    defer kv.Close()

    kv.SetName("CommandConfig")
    // ... использовать kv ...
    // Очищается defer - безопасно!
}

✅ Безопасно: Переменная пакета с очисткой

Безопасный паттерн
package main

var pluginConfig *test_keyvalues.KeyValues

func PluginStart() {
    pluginConfig = test_keyvalues.NewKeyValuesKv1Create("PluginConfig")
    pluginConfig.SetName("Settings")
}

func PluginEnd() {
    // Очистить переменные пакета
    if pluginConfig != nil {
        pluginConfig.Close()
        pluginConfig = nil
    }
}

❌ Небезопасно: Глобальная без очистки

Небезопасный паттерн
package main

import "test_keyvalues"

// ОПАСНО: Глобальный объект
var gConfig = test_keyvalues.NewKeyValuesKv1Create("GlobalConfig")

func PluginStart() {
    gConfig.SetName("ServerSettings")
}

func PluginEnd() {
    // ОТСУТСТВУЕТ: Нет очистки!
    // Финализатор gConfig запустится после выгрузки плагина - СБОЙ!
}

❌ Небезопасно: Map уровня пакета без очистки

Небезопасный паттерн
package main

import "test_keyvalues"

// ОПАСНО: Кэш уровня пакета
var kvCache = make(map[string]*test_keyvalues.KeyValues)

func cacheConfig(name string) {
    kvCache[name] = test_keyvalues.NewKeyValuesKv1Create(name)
}

func PluginEnd() {
    // ОТСУТСТВУЕТ: Нет очистки кэша!
    // Кэшированные объекты завершатся после выгрузки плагина - СБОЙ!
}

Контрольный список очистки

Перед выгрузкой вашего плагина (PluginEnd()), убедитесь:

  1. ✅ Все глобальные экземпляры классов явно закрыты или установлены в nil
  2. ✅ Все map/срезы уровня пакета, содержащие экземпляры классов, очищены
  3. ✅ Не осталось ссылок на экземпляры классов в долгоживущих структурах данных
  4. ✅ Вы вызвали Close() для всех принадлежащих объектов

Полный пример: Система конфигурации

Вот полный пример, показывающий, как использовать классы для системы конфигурации:

config_manager.go
package main

import (
    "errors"
    "test_keyvalues"
)

type ConfigManager struct {
    root *test_keyvalues.KeyValues
}

func NewConfigManager(configName string) (*ConfigManager, error) {
    root := test_keyvalues.NewKeyValuesKv1Create(configName)

    if !root.IsValid() {
        return nil, errors.New("failed to create config: " + configName)
    }

    return &ConfigManager{root: root}, nil
}

func (cm *ConfigManager) Close() {
    if cm.root != nil {
        cm.root.Close()
        cm.root = nil
    }
}

func (cm *ConfigManager) CreateSection(sectionName string) error {
    section := test_keyvalues.NewKeyValuesKv1Create(sectionName)

    if !section.IsValid() {
        return errors.New("failed to create section: " + sectionName)
    }

    return cm.root.AddSubKey(section)
}

func (cm *ConfigManager) GetSection(sectionName string) (*test_keyvalues.KeyValues, error) {
    return cm.root.FindKey(sectionName)
}

func (cm *ConfigManager) SetValue(sectionName, key, value string) error {
    section, err := cm.GetSection(sectionName)
    if err != nil {
        return err
    }
    defer section.Close()

    return section.SetString(key, value)
}

func (cm *ConfigManager) GetValue(sectionName, key, defaultValue string) (string, error) {
    section, err := cm.GetSection(sectionName)
    if err != nil {
        return defaultValue, err
    }
    defer section.Close()

    return section.GetString(key, defaultValue)
}

func (cm *ConfigManager) Save(filename string) error {
    if !cm.root.IsValid() {
        return errors.New("invalid config root")
    }
    return cm.root.SaveToFile(filename)
}

func (cm *ConfigManager) Load(filename string) error {
    if !cm.root.IsValid() {
        return errors.New("invalid config root")
    }
    return cm.root.LoadFromFile(filename)
}

// Использование
func useConfigManager() error {
    config, err := NewConfigManager("ServerConfig")
    if err != nil {
        return err
    }
    defer config.Close()  // Очистка при завершении

    config.CreateSection("Server")
    config.SetValue("Server", "hostname", "My Server")
    config.SetValue("Server", "maxplayers", "32")

    config.CreateSection("Game")
    config.SetValue("Game", "mode", "competitive")

    return config.Save("config.kv")
}

Лучшие практики

  1. Всегда используйте defer - Вызывайте defer obj.Close() сразу после создания принадлежащих объектов
  2. Проверяйте ошибки - Все методы возвращают ошибки, всегда проверяйте их
  3. Очищайте глобальные в PluginEnd() - Критично: Явно очищайте все глобальные или уровня пакета экземпляры классов перед выгрузкой плагина, чтобы избежать сбоев
  4. Не defer освобожденные объекты - Если метод принимает владение, не defer очистку на переданном объекте
  5. Предпочитайте локальную область видимости - Держите экземпляры классов в локальной области видимости с defer, когда возможно
  6. Проверяйте IsValid() - Особенно после операций, которые могут завершиться неудачей
  7. Уважайте владение - Не используйте объекты после передачи владения (они автоматически освобождаются)
  8. Не копируйте - Защита noCopy обнаружит копии с помощью go vet
  9. Используйте возврат ошибок - Не паникуйте в библиотечном коде, возвращайте ошибки
  10. Обрабатывайте nil возвраты - Методы могут возвращать nil при неудаче
  11. Запускайте go vet - Обнаруживает случайные копии и другие проблемы

Когда НЕ использовать классы

Классы предназначены для ресурсов, которым требуется управление жизненным циклом. Не определяйте классы для:

  • Функции-утилиты без состояния - Простые функции, которые не управляют ресурсами
  • Функции, возвращающие примитивные значения - Нет необходимости оборачивать простые геттеры
  • Одноразовые операции - Операции, которые не поддерживают состояние
  • Глобальные синглтоны - Ресурсы, которые живут в течение всего времени работы программы

Для этих случаев продолжайте использовать обычный API на основе функций.

Устранение неполадок

Ошибка "empty handle"

Если метод возвращает ошибку пустого дескриптора, базовая функция создания вернула недопустимый дескриптор:

Сбой конструктора дескриптора
package main

import (
    "fmt"
    "test_keyvalues"
)

func handleFailure() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    defer kv.Close()

    if !kv.IsValid() {
        fmt.Println("Failed to create KeyValues")
        return
    }

    // ... использовать kv ...
}

Использование закрытых или освобожденных объектов

Попытка использовать объект после вызова Close() или Release() вернет ошибку:

Использование закрытого дескриптора
package main

import (
    "fmt"
    "test_keyvalues"
)

func useClosedHandle() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    kv.Close()

    // Это вернет ошибку
    if err := kv.SetName("Test"); err != nil {
        fmt.Printf("Error: %v\n", err)  // "KeyValues: empty handle"
    }

    // Сначала проверьте IsValid(), чтобы избежать ошибок
    if kv.IsValid() {
        kv.SetName("Test")
    } else {
        fmt.Println("Handle is closed!")
    }
}

Все связанные методы автоматически проверяют дескриптор перед вызовом базовой функции C.

Висячие ссылки

Будьте осторожны с невладеющими ссылками, когда владелец уничтожается:

Проблема висячей ссылки
package main

import "test_keyvalues"

func getChildRef() *test_keyvalues.KeyValues {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")
    defer parent.Close()

    child, _ := parent.GetFirstSubKey()  // Невладеющая ссылка
    return child  // ПЛОХО: parent будет уничтожен!
}

// Это опасно!
func useDanglingRef() {
    childRef := getChildRef()
    // childRef теперь указывает на уничтоженную память
}

// Лучший подход:
func getChildOwned() *test_keyvalues.KeyValues {
    parent := test_keyvalues.NewKeyValuesKv1Create("Parent")

    child, err := parent.FindKey("Child")  // Возвращает принадлежащий экземпляр
    if err != nil {
        parent.Close()
        return nil
    }

    // Очистка parent отложена - нужно обрабатывать осторожно
    // Лучше использовать в той же области видимости функции
    return child
}

Случайные копии

Защита noCopy предотвращает случайные копии:

Обнаружение копирования
package main

import "test_keyvalues"

func accidentalCopy() {
    kv := test_keyvalues.NewKeyValuesKv1Create("Config")
    defer kv.Close()

    // Это будет обнаружено go vet!
    // kvCopy := *kv  // ОШИБКА: copies lock value

    // Используйте указатели вместо этого
    kvPtr := kv  // OK
    _ = kvPtr
}

Запустите go vet для обнаружения этих проблем:

go vet ./...

Сбои при выгрузке плагина

Если ваш плагин падает при выгрузке:

  1. Проверьте глобальные переменные - Убедитесь, что все глобальные экземпляры классов очищены в PluginEnd()
  2. Проверьте map/срезы уровня пакета - Очистите все коллекции, содержащие экземпляры классов
  3. Добавьте явную очистку - Используйте Close() для всех долгоживущих объектов перед выгрузкой плагина
  4. Запустите с детектором гонок - go build -race может обнаружить некоторые проблемы
Исправление сбоя при выгрузке
package main

import (
    "github.com/untrustedmodders/go-plugify"
    "test_keyvalues"
)

// Глобальные объекты, которые вызывали сбои
var gConfig *test_keyvalues.KeyValues
var gCache = make(map[string]*test_keyvalues.KeyValues)

func PluginStart() {
    gConfig = test_keyvalues.NewKeyValuesKv1Create("Config")
    gCache["main"] = test_keyvalues.NewKeyValuesKv1Create("Main")
}

func PluginEnd() {
    // Очистить глобальные объекты
    if gConfig != nil {
        gConfig.Close()
        gConfig = nil
    }

    // Очистить кэшированные объекты
    for key, kv := range gCache {
        kv.Close()
        delete(gCache, key)
    }

    plugify.Log("All resources cleaned up safely")
}

См. также

  • Узнайте, как экспортировать свои собственные классы
  • Узнайте об импорте функций
  • Справочник по манифесту плагина