chi.URLParam not working when handler is defined outside main package - go

So I am new to go and I currently try to build a little REST-API using chi (and I love it). Yesterday I run into a problem, that I cannot quite understand.
In my little test-project I have a main.go file which contains the main function with router instantiation, adding middlewares and starting the server:
func main() {
router := chi.NewRouter()
// Middleware
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
router.Use(middleware.Logger)
router.Use(middleware.Recoverer)
// Routes
router.Post("/login", users.Login)
router.Post("/register", users.Register)
router.With(users.LoginRequired).Route("/users", func(r chi.Router) {
r.Get("/{user_id}", users.GetUser)
})
// Start Server
port := ":8080"
log.Printf("Server starting at port %v\n", port)
log.Fatal(http.ListenAndServe(port, router))
}
First the problem didn't exist because I defined all the handler functions within my main.go file and the GetUser-function worked as expected and returned a user from my "Database" (array with 3 users):
func GetUser(w http.ResponseWriter, r *http.Request) {
uID := chi.URLParam(r, "user_id") // Problem when not in main -> uID = ""
id, err := strconv.Atoi(uID)
if err != nil {
log.Printf("Error while parsing int: %v\n", err)
// TODO: return error 400
}
user := DataBase[id-1]
response, err := json.Marshal(user)
if err != nil {
log.Printf("Error while marshalling user: %v\n", err)
}
w.Write(response)
}
As soon as I moved this function out of the main.go file into another package called users the chi.URLParam function returns an empty string and cannot find the URLParam anymore. I read it has something to do with the context, but I cannot wrap my head around that I have to place functions inside the main-file if I want to use the chi functions.
Am I missing something?
UPDATE
As requested I removed everything except the GetUser function. My main.go file currently looks like this:
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/MyUserName/MyProjectName/internals/users"
"github.com/go-chi/chi/v5"
)
func GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "user_id"))
if err != nil {
log.Printf("Error while parsing int: %v\n", err)
// TODO: return error 400
}
log.Printf("ID=%v, Current Database=%v\n", id, users.DataBase)
user := users.DataBase[id-1]
response, err := json.Marshal(user)
if err != nil {
log.Printf("Error while marshalling user: %v\n", err)
}
w.Write(response)
}
func main() {
router := chi.NewRouter()
// Routes
router.Get("/users/{user_id}", GetUser)
// Start Server
port := ":8080"
log.Printf("Server starting at port %v\n", port)
log.Fatal(http.ListenAndServe(port, router))
}
and my users package looks like this:
package users
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/MyUserName/MyProjectName/internals/models"
"github.com/go-chi/chi"
)
var (
DataBase = make([]models.User, 0)
)
func GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "user_id"))
if err != nil {
log.Printf("Error while parsing int: %v\n", err)
// TODO: return error 400
}
log.Printf("ID=%v, Current Database=%v\n", id, DataBase)
user := DataBase[id-1]
response, err := json.Marshal(user)
if err != nil {
log.Printf("Error while marshalling user: %v\n", err)
}
w.Write(response)
}
func init() {
initUser := []models.User{
{
ID: 1,
UserName: "john",
Password: "doe",
},
{
ID: 2,
UserName: "max",
Password: "mustermann",
},
{
ID: 3,
UserName: "jane",
Password: "doe",
},
}
for _, user := range initUser {
DataBase = append(DataBase, user)
}
log.Println("Initializing Database")
}
When I use the function from the users package it does not work and is still an empty string, if I use the function from the main.go file it works.
UPDATE
So apparently I am to stupid to import the same packages twice. In my main file I used "github.com/go-chi/chi/v5" and in my users package I used "github.com/go-chi/chi". Using the same resolved the issue, thanks a lot

Adding answer because the comments just saved me!
Check that all files in your go solution have the same version of chi in use. If you're using VSCode it may import a different version than you expect. In my code I had one file with
import(
"github.com/go-chi/chi"
)
and in the other
import(
"github.com/go-chi/chi/v5"
)
This meant that when I was calling into middleware function to extract URLParams the context was not finding a value.
TL;DR
Check that all files use same version of Chi!

Related

go build doesn't recognise methods

I try to setup a small Golang Microservice for users with Gin and Mongodb.
package main
import (
"context"
"fmt"
"github.com/wzslr321/artiver/entity"
"github.com/wzslr321/artiver/settings"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"log"
"os"
"os/signal"
"syscall"
"time"
)
type application struct {
users *entity.UserCollection
}
var app *application
func init() {
initMongo()
}
func initMongo() {
oc := options.Client().ApplyURI(settings.MongodbSettings.Uri)
client, err := mongo.NewClient(oc)
if err != nil {
log.Fatalf("Error occured while initializing a new mongo client: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
err = client.Connect(ctx)
if err != nil {
log.Fatalf("Errorr occurred while connecting to a client: %v", err)
}
defer func() {
if err = client.Disconnect(ctx); err != nil {
panic(err)
}
}()
log.Println("Successfully connected to the database!")
app = &application{
users: &entity.UserCollection{
C: client.Database("artiver").Collection("users"),
},
}
}
func main() {
router := app.InitRouter()
It doesn't show any errors in my IDE ( GoLand ), but when I try to build it I get an error:
# command-line-arguments
users/cmd/app/main.go:67:15: app.InitRouter undefined (type *application has no field or method InitRouter)
It it easily visible on the image above, that I do have access to such a method. It is defined in the same package.
package main
import (
"github.com/gin-gonic/gin"
cors "github.com/rs/cors/wrapper/gin"
"net/http"
)
func (app *application) InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Recovery())
r.Use(cors.Default())
r.GET("/", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "Hello World")
})
user := r.Group("/api/user")
{
user.POST("/add", app.CreateUser)
}
return r
}
I have no idea how am I supposed to fix it and what is done wrong. I'd appreciate any hint about what isn't done correctly.
Answer based on #mkopriva help in comments.
The issue was related to not running all needed .go files.
In my case, the solution was to build it this way in my Makefile:
go build -o $(path)users cmd/app/*
In similar cases, go run . most likely will do the job.

How to validate API key in go-gin framework?

So I currently have a function that will take in a string APIKey to check it against my MongoDB collection. If nothing is found (not authenticated), it returns false - if a user is found, it returns true. My problem, however, is I'm unsure how to integrate this with a Gin POST route. Here is my code:
import (
"context"
"fmt"
"log"
"os"
"github.com/gin-gonic/gin"
_ "github.com/joho/godotenv/autoload"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type User struct {
Name string
APIKey string
}
func validateAPIKey(users *mongo.Collection, APIKey string) bool {
var user User
filter := bson.D{primitive.E{Key: "APIKey", Value: APIKey}}
if err := users.FindOne(context.TODO(), filter).Decode(&user); err != nil {
fmt.Printf("Found 0 results for API Key: %s\n", APIKey)
return false
}
fmt.Printf("Found: %s\n", user.Name)
return true
}
func handleUpload(c *gin.Context) {
}
func main() {
r := gin.Default()
api := r.Group("/api")
v1 := api.Group("/v1")
v1.POST("/upload", handleUpload)
mongoURI := os.Getenv("MONGO_URI")
mongoOptions := options.Client().ApplyURI(mongoURI)
client, err := mongo.Connect(context.TODO(), mongoOptions)
if err != nil {
log.Fatal(err, "Unable to access MongoDB server, exiting...")
}
defer client.Disconnect(context.TODO())
// users := client.Database("sharex_api").Collection("authorized_users") // commented out when testing to ignore unused warnings
r.Run(":8085")
}
The validateAPIKey function works exactly as intended if tested alone, I am just unsure how I would run this function for a specific endpoint (in this case, /api/v1/upload) and pass in the users collection.
After a bit of searching, I found a resolution. I changed my validateAPIKey function to return git.HandlerFunc. Here's the code:
func validateAPIKey(users *mongo.Collection) gin.HandlerFunc {
return func(c *gin.Context) {
var user authorizedUser
APIKey := c.Request.Header.Get("X-API-Key")
filter := bson.D{primitive.E{Key: "APIKey", Value: APIKey}}
if err := users.FindOne(context.TODO(), filter).Decode(&user); err != nil {
fmt.Printf("Found 0 results for API Key: %s\n", APIKey)
c.JSON(http.StatusUnauthorized, gin.H{"status": 401, "message": "Authentication failed"})
return
}
return
}
}
For the route, I have the following:
v1.POST("/upload", validateAPIKey(users), handleUpload)

How correctly to make controllers to routes in Golang?

I am new in Golang and need some help. I am tring to create REST API web service without ORM.
Right now I am successfully connected to PostgreSQL database. In database I have table which called factors. I want to create CRUD operations. The problem is with controllers logic.
main.go:
package main
import (
"github.com/gorilla/mux"
"log"
"net/http"
"rest_api/configurations"
"rest_api/controllers"
)
func main() {
db, err := configurations.PostgreSQLDatabase()
if err != nil {
log.Fatal(err)
}
router := mux.NewRouter()
router.StrictSlash(true)
subrouter := router.PathPrefix("/api").Subrouter()
subrouter.HandleFunc("/factors", controllers.GetFactors(db)).Methods("GET")
log.Fatal(http.ListenAndServe(":8000", router))
}
models/factors.go:
package models
type Factor struct {
ID int `json:"id"`
Name string `json:"name"`
}
How correctly looks like the GetFactors controller? Can someone show me please. For instance I pass db object to GetFactors controller as in the example below. Unfortunately it seems like it's incorrect.
controllers/factors.go:
func GetFactors(db *sql.DB, w http.ResponseWriter, req *http.Request) {
// some code
}
configurations/PostgreSQL.go:
func PostgreSQLDatabase() (*sql.DB, error) {
// Load environment variables from ".env" file.
err := godotenv.Load(".env")
if err != nil {
log.Fatal(err)
}
// Initialize database-related variables.
dbUser := os.Getenv("PostgreSQL_USER")
dbPassword := os.Getenv("PostgreSQL_PASSWORD")
dbHost := os.Getenv("PostgreSQL_HOST")
dbName := os.Getenv("PostgreSQL_DB_NAME")
dbURL := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable", dbUser, dbPassword, dbHost, dbName)
// Create PostgreSQL database connection pool.
db, err := sql.Open("postgres", dbURL)
if err != nil {
return nil, err
}
// Ping PostgreSQL database to make sure it's alive.
err = db.Ping()
if err != nil {
log.Fatal(err)
} else {
log.Println("Web service successfully connected to remote PostgreSQL database.")
}
return db, nil
}
A pattern I like to use is to define your own Router struct that has a mux.Router as a field as well as encapsulates things like your database connection, application config and etc.
I find doing it this way makes it easily update your routes when they require different resources and development proceeds.
First create a router object that takes in the database connection on creation and makes it available to all routes you wish to use.
router.go
package main
import (
"net/http"
"database/sql"
"github.com/gorilla/mux"
)
type Router struct {
router *mux.Router
db *sql.DB
}
func NewRouter(db *sql.DB) (*Router, error) {
router := mux.NewRouter()
router.StrictSlash(true)
subrouter := router.PathPrefix("/api").Subrouter()
r := &Router{
router: router,
db: db,
}
subrouter.HandleFunc("/factors", r.GetFactors).Methods(http.MethodGet)
return r, nil
}
func (r *Router) GetFactors(w http.ResponseWriter, req *http.Request) {
// Now you can access your database via `r.db`
}
// Needed so we can pass our custom router to ListenAndServe.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.router.ServeHTTP(w, req)
}
Then in main.go you can simply create your custom router, passing it your database connection. Then the custom router can be passed directly to ListenAndServe.
main.go
package main
import (
"log"
"net/http"
"rest_api/configurations"
"rest_api/controllers"
)
func main() {
db, err := configurations.PostgreSQLDatabase()
if err != nil {
log.Fatal(err)
}
router, err := NewRouter(db)
if err != nil {
log.Fatalf("error initializing router: %v", err)
}
log.Fatal(http.ListenAndServe(":8000", router))
}
Hopefully this helps.
Your func GetFactors must looks like:
func GetFactors(w http.ResponseWriter, r *http.Request) {}
and in main file you must have:
subrouter.HandleFunc("/factors", controllers.GetFactors).Methods("GET")
and with purpose to get DB connection you must have func like GetDB in your package "rest_api/configurations".
In "rest_api/configurations" you must have something like:
var db *PostgreSQLDatabase
func init() {
var err error
db, err = configurations.PostgreSQLDatabase()
if err != nil {
log.Fatal(err)
}
}
func GetDB() *PostgreSQLDatabase {
return db
}
There is no correct way, it mostly opinion-based.
The semantic of HandlerFunc function should be like func(w http.ResponseWriter, r *http.Request), in order to pass database you can use closures, here is an example.
main.go
// ... some code here
subrouter.HandleFunc("/factors", controllers.GetFactors(db)).Methods("GET")
// ... some code here
controllers/factors.go
func GetFactors(db *sql.DB) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// some code
})
}
Another option:
I'm not quite sure about this, but you can adjucts it to your needs. Initialize a Controller struct and pass db to it:
main.go
// ... some code here
db, err := configurations.PostgreSQLDatabase()
if err != nil {
log.Fatal(err)
}
ctrl := controllers.Controller{DB: db}
subrouter.HandleFunc("/factors", ctrl.GetFactors).Methods("GET")
// ... some code here
Denote a method on the Controller struct.
Define a struct in the controllers
controllers/factors.go
type Controller struct {
DB *PostgreSQLDatabase
}
func (c Controller) GetFactors(w http.ResponseWriter, req *http.Request) {
// some code
// c.DB.MySqlMethod()
}

Connection DB in golang

Where i can put initialize files like languages, connection db etc. in mvc structur golang (beego,revel)?
I tried to use in controller but it isn't good.
Is a good solution would be create base controller and put here all init connection, languages etc? or is there some other way (better)?
You can use global variables, but I don't suggest to do it. What happens in more complicated applications where database logic is spread over multiple packages? It's better to use dependency injection:
File: main.go
package main
import (
"bookstore/models"
"database/sql"
"fmt"
"log"
"net/http"
)
type Env struct {
db *sql.DB
}
func main() {
db, err := models.NewDB("postgres://user:pass#localhost/bookstore")
if err != nil {
log.Panic(err)
}
env := &Env{db: db}
http.HandleFunc("/books", env.booksIndex)
http.ListenAndServe(":3000", nil)
}
func (env *Env) booksIndex(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
bks, err := models.AllBooks(env.db)
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
for _, bk := range bks {
fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.Isbn, bk.Title, bk.Author, bk.Price)
}
}
File: models/db.go
package models
import (
"database/sql"
_ "github.com/lib/pq"
)
func NewDB(dataSourceName string) (*sql.DB, error) {
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}
I always do some packega where i keep my enviroment variables.
For example main.go
package main
import (
"net/http"
env "github.com/vardius/example/enviroment"
)
func main() {
//some extra code here, http srever or something
defer env.DB.Close()
}
end inside enviroment dir env.go
package env
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
var (
DB *sql.DB
)
func connectToDB(dbURL string) *sql.DB {
conn, err := sql.Open("mysql", dbURL)
//check for err
return conn
}
func init() {
DB = connectToDB("root:password#tcp(127.0.0.1:3306)/test")
}
this way you initialize once your DB and can use it in all parts of your app by injecting env
Ofcourse this solution has some downsides. First, code is harder to
ponder because the dependencies of a component are unclear. Second,
testing these components is made more difficult, and running tests in
parallel is near impossible. With global connections, tests that hit
the same data in a backend service could not be run in parallel.
There is a great article about a Dependency Injection with Go
I hope you will find this helpfull

Missing symbols when importing `github.com/influxdb/influxdb/client/v2` package

Setting up a web socket on google cloud in Golang, and import code that works fine on my local machine does not work on the cloud.
I have:
import "github.com/influxdb/influxdb/client/v2"
and have run
go get "github.com/influxdb/influxdb/client/v2"
Upon running go run server.go I get:
# command-line-arguments
./pi_server.go:47: undefined: client.NewClient
./pi_server.go:47: undefined: client.Config
Full code below, excluding const declarations and html:
package main
import (
"flag"
"html/template"
"log"
"net/http"
"github.com/gorilla/websocket"
"fmt"
"net/url"
"github.com/influxdb/influxdb/client/v2"
"time"
)
var addr = flag.String("addr", "localhost:8080", "http service address")
var upgrader = websocket.Upgrader{} // use default options
func echo(w http.ResponseWriter, r *http.Request) {
//Influx init
u,err := url.Parse("http://localhost:8086")
checkError(err)
influx_c := client.NewClient(client.Config{
URL: u,
Username: username,
Password: password,
})
bp,err := client.NewBatchPoints(client.BatchPointsConfig{
Database: MyDB,
Precision: "s",
})
tags := map[string]string{"my_sensor_id": my_sensor_id}
//end influx init
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
/*
write to influx here
*/
fields := map[string]interface{}{
"random_int": message,
"other_stuff": 69696,
}
pt,err := client.NewPoint("test_collection", tags, fields, time.Now())
checkError(err)
bp.AddPoint(pt)
influx_c.Write(bp)
err = c.WriteMessage(mt, message)
if err != nil {
log.Println("write:", err)
break
}
}
}
func home(w http.ResponseWriter, r *http.Request) {
homeTemplate.Execute(w, "ws://"+r.Host+"/echo", )
}
func main() {
flag.Parse()
log.SetFlags(0)
http.HandleFunc("/echo", echo)
http.HandleFunc("/", home)
log.Fatal(http.ListenAndServe(*addr, nil))
}
You local machine has a version of github.com/influxdb/influxdb/client/v2 before this commit. Your cloud server is fetching a more recent version of the package.
To fix the issue, run
go get -u github.com/influxdb/influxdb/client/v2
on your local machine to get the latest version of the package. Update the application code to use the new function and type names:
influx_c := client.NewHTTPClient(client.HTTPConfig{
URL: u,
Username: username,
Password: password,
})
Nailed it, thanks! Also note from following code:
influx_c,err := client.NewHTTPClient(client.HTTPConfig{
Addr: "http://localhost:8086",
Username: username,
Password: password,
})
They changed URL field to Addr, with is a string literal instead of a net/url object

Resources