©Sergey Emelyanov 2025 | Alle Rechte vorbehalten
In der heutigen Welt hilft die Automatisierung von Routineprozessen, Zeit zu sparen und Fehler zu vermeiden. In einem meiner Projekte hatte ich die Aufgabe, SQL-Skripte, die in einem bestimmten Ordner auftauchen, automatisch auszuführen und dann einen vollständigen Bericht über die Ausführung an die E-Mail des Administrators zu senden. Um diese Aufgabe zu lösen, beschloss ich, einen spezialisierten Microservice in der Sprache Go zu schreiben, der das angegebene Verzeichnis ständig überwacht, die gefundenen SQL-Dateien ausführt und die zuständigen Personen über die Ergebnisse der Arbeit benachrichtigt. Im Folgenden werde ich die Schritte der Implementierung, die Verwendung einer externen Konfigurationsdatei und die Besonderheiten des Ansatzes im Detail beschreiben.
Der erste Schritt bestand darin, die Projektstruktur zu entwickeln und eine Konfigurationsdatei zu erstellen. Um flexibel arbeiten zu können, werden alle Einstellungen - Datenbankverbindungsparameter, Informationen für SMTP-Benachrichtigungen und Pfade zu Arbeitsordnern - in einer separaten Datei config.yml abgelegt. Das macht es einfach, die Parameter zu ändern, ohne den Quellcode zu verändern.
database:
host: localhost
port: 3306
user: root
password: secret
name: mydb
smtp:
server: smtp.example.com
port: 587
username: user@example.com
password: smtp-password
from: noreply@example.com
to: admin@example.com
paths:
input: ./sql/input
output: ./sql/processed
error: ./sql/errors
Nachdem Sie die Einstellungen aus der Datei geladen haben, müssen Sie eine Verbindung zur Datenbank herstellen. Zu diesem Zweck habe ich ein Paket für die Arbeit mit MySQL verwendet. Es ist notwendig, eine Verbindung zur Datenbank herzustellen und die Korrektheit der Interaktion mit der Datenbank zu überprüfen.
func initDB() {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
config.Database.User,
config.Database.Password,
config.Database.Host,
config.Database.Port,
config.Database.Name)
var err error
db, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error connecting to database: %v", err)
}
err = db.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
}
Die Hauptfunktion des Skripts besteht darin, ein bestimmtes Verzeichnis ständig zu überwachen. Alle paar Sekunden prüft die Anwendung, ob neue SQL-Dateien vorhanden sind, und wenn eine gefunden wird, liest sie den Inhalt der Datei und versucht, SQL-Abfragen in der Datenbank auszuführen.
Die Merkmale dieser Phase sind:
func processFiles() {
files, err := os.ReadDir(config.Paths.Input)
if err != nil {
log.Printf("Error reading input directory: %v", err)
return
}
for _, file := range files {
if file.IsDir() {
continue
}
filePath := filepath.Join(config.Paths.Input, file.Name())
processFile(filePath)
}
}
func processFile(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
log.Printf("Error reading file %s: %v", filePath, err)
return
}
err = executeSQL(string(data))
if err != nil {
handleError(filePath, err)
} else {
handleSuccess(filePath)
}
}
Jede SQL-Datei wird vor der Ausführung geprüft. Danach wird eine SQL-Abfrage mit db.Exec ausgeführt. Wenn die Abfrage erfolgreich ausgeführt wurde, wird die Datei in den Ordner für archivierte Skripte verschoben; andernfalls wird sie in den Ordner für Fehler verschoben. Auf diese Weise wird die wiederholte Ausführung derselben Abfrage vermieden, und es kann festgestellt werden, welche Dateien wiederholt bearbeitet werden müssen.
func executeSQL(query string) error {
if strings.Contains(query, "\ufeff") {
query = strings.ReplaceAll(query, "\ufeff", "")
}
_, err := db.Exec(query)
return err
}
Am Ende der Skriptoperation ist es wichtig, einen Bericht an den Administrator zu senden. Jedes Mal, wenn ein Vorgang ausgeführt wurde (erfolgreich oder mit einem Fehler), wird eine entsprechende Benachrichtigung über SMTP gesendet. Durch die Verwendung einer sicheren TLS-Verbindung wird die Sicherheit der Datenübertragung gewährleistet. So erhalten Sie schnell eine Rückmeldung über den Skriptbetrieb und können möglichen Problemen im Systembetrieb vorbeugen.
func handleError(filePath string, err error) {
log.Printf("Error executing SQL: %v", err)
sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
moveFile(filePath, config.Paths.Failed)
}
Vorteile:
Nachteilig:
Schlussfolgerung
Die Entwicklung dieses Microservices war für mich eine einzigartige Erfahrung bei der Automatisierung von Routineaufgaben mit Go. Die ordnungsgemäße Trennung der Logik in Konfiguration, Datenbankkonnektivität, Dateiüberwachung und Benachrichtigungen macht das System flexibel und skalierbar. Dieser Ansatz minimiert nicht nur die Risiken, die mit der manuellen Ausführung von SQL-Skripten verbunden sind, sondern ermöglicht auch eine schnelle Reaktion auf Probleme durch Berichte.
Wenn Sie vor ähnlichen Herausforderungen stehen, kann dieses Beispiel ein guter Ausgangspunkt für die Entwicklung einer robusten und benutzerfreundlichen Lösung sein.
package main
import (
"crypto/tls"
"database/sql"
"fmt"
_ "github.com/octoper/go-ray"
"log"
"net/smtp"
"os"
"path/filepath"
"strings"
"time"
_ "github.com/go-sql-driver/mysql"
"gopkg.in/yaml.v3"
)
type Config struct {
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
Name string `yaml:"name"`
} `yaml:"database"`
SMTP struct {
Server string `yaml:"server"`
Port int `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
From string `yaml:"from"`
To string `yaml:"to"`
} `yaml:"smtp"`
Paths struct {
Input string `yaml:"input"`
Done string `yaml:"done"`
Failed string `yaml:"failed"`
} `yaml:"paths"`
}
var (
config Config
db *sql.DB
)
func main() {
loadConfig("config.yml")
initDB()
defer db.Close()
for {
processFiles()
time.Sleep(5 * time.Second) // Проверка каждые 5 секунд
}
}
func loadConfig(filename string) {
data, err := os.ReadFile(filename)
if err != nil {
log.Fatalf("Error reading config file: %v", err)
}
err = yaml.Unmarshal(data, &config)
if err != nil {
log.Fatalf("Error parsing config file: %v", err)
}
}
func initDB() {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
config.Database.User,
config.Database.Password,
config.Database.Host,
config.Database.Port,
config.Database.Name)
var err error
db, err = sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error connecting to database: %v", err)
}
err = db.Ping()
if err != nil {
log.Fatalf("Error pinging database: %v", err)
}
}
func processFiles() {
files, err := os.ReadDir(config.Paths.Input)
if err != nil {
log.Printf("Error reading input directory: %v", err)
return
}
for _, file := range files {
if file.IsDir() {
continue
}
filePath := filepath.Join(config.Paths.Input, file.Name())
processFile(filePath)
}
}
func processFile(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
log.Printf("Error reading file %s: %v", filePath, err)
return
}
err = executeSQL(string(data))
if err != nil {
handleError(filePath, err)
} else {
handleSuccess(filePath)
}
}
func executeSQL(query string) error {
if strings.Contains(query, "\ufeff") {
query = strings.ReplaceAll(query, "\ufeff", "")
}
_, err := db.Exec(query)
return err
}
func handleError(filePath string, err error) {
log.Printf("Error executing SQL: %v", err)
sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
moveFile(filePath, config.Paths.Failed)
}
func handleSuccess(filePath string) {
log.Println("SQL executed successfully")
sendEmail("SQL Execution Success", fmt.Sprintf("File: %s", filePath))
moveFile(filePath, config.Paths.Done)
}
func moveFile(source, destDir string) {
fileName := filepath.Base(source)
destPath := filepath.Join(destDir, fileName)
err := os.Rename(source, destPath)
if err != nil {
log.Printf("Error moving file: %v", err)
}
}
func sendEmail(subject, body string) {
tlsConfig := &tls.Config{
ServerName: config.SMTP.Server,
InsecureSkipVerify: false,
}
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", config.SMTP.Server, config.SMTP.Port), tlsConfig)
if err != nil {
log.Printf("Error creating TLS connection: %v", err)
return
}
defer conn.Close()
client, err := smtp.NewClient(conn, config.SMTP.Server)
if err != nil {
log.Printf("Error creating SMTP client: %v", err)
return
}
defer client.Close()
auth := smtp.PlainAuth("", config.SMTP.Username, config.SMTP.Password, config.SMTP.Server)
if err := client.Auth(auth); err != nil {
log.Printf("SMTP auth error: %v", err)
return
}
if err := client.Mail(config.SMTP.From); err != nil {
log.Printf("Mail command error: %v", err)
return
}
if err := client.Rcpt(config.SMTP.To); err != nil {
log.Printf("Rcpt command error: %v", err)
return
}
wc, err := client.Data()
if err != nil {
log.Printf("Data command error: %v", err)
return
}
defer wc.Close()
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s",
config.SMTP.From,
config.SMTP.To,
subject,
body,
)
if _, err = fmt.Fprint(wc, msg); err != nil {
log.Printf("Error writing message: %v", err)
return
}
}
©Sergey Emelyanov 2025 | Alle Rechte vorbehalten